From 8c946dfe805e480c5239cee76a27f20c6d048367 Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Fri, 2 Jan 2026 16:51:37 +0530 Subject: [PATCH 01/75] feat: enhance document upload UI with accordion functionality --- .../assistant-ui/document-upload-popup.tsx | 40 +++++++++++++++---- .../components/sources/DocumentUploadTab.tsx | 26 ++++++++---- 2 files changed, 51 insertions(+), 15 deletions(-) diff --git a/surfsense_web/components/assistant-ui/document-upload-popup.tsx b/surfsense_web/components/assistant-ui/document-upload-popup.tsx index d1fa208d2..9734954e1 100644 --- a/surfsense_web/components/assistant-ui/document-upload-popup.tsx +++ b/surfsense_web/components/assistant-ui/document-upload-popup.tsx @@ -1,5 +1,6 @@ "use client"; +import { Upload } from "lucide-react"; import { useAtomValue } from "jotai"; import { useRouter } from "next/navigation"; import { @@ -85,6 +86,7 @@ const DocumentUploadPopupContent: FC<{ }> = ({ isOpen, onOpenChange }) => { const searchSpaceId = useAtomValue(activeSearchSpaceIdAtom); const router = useRouter(); + const [isAccordionExpanded, setIsAccordionExpanded] = useState(false); if (!searchSpaceId) return null; @@ -95,16 +97,40 @@ const DocumentUploadPopupContent: FC<{ return ( - + Upload Document -
-
-
- + + {/* Fixed Header */} +
+ {/* Upload header */} +
+
+ +
+
+

Upload Documents

+

+ Upload and sync your documents to your search space +

- {/* Bottom fade shadow */} -
+
+ + {/* Scrollable Content */} +
+
+
+ +
+
+ {/* Bottom fade shadow - only show when scrolling */} + {isAccordionExpanded && ( +
+ )}
diff --git a/surfsense_web/components/sources/DocumentUploadTab.tsx b/surfsense_web/components/sources/DocumentUploadTab.tsx index 5280ea850..0062dd2dc 100644 --- a/surfsense_web/components/sources/DocumentUploadTab.tsx +++ b/surfsense_web/components/sources/DocumentUploadTab.tsx @@ -31,6 +31,7 @@ import { GridPattern } from "./GridPattern"; interface DocumentUploadTabProps { searchSpaceId: string; onSuccess?: () => void; + onAccordionStateChange?: (isExpanded: boolean) => void; } const audioFileTypes = { @@ -109,11 +110,12 @@ const FILE_TYPE_CONFIG: Record> = { const cardClass = "border border-border bg-slate-400/5 dark:bg-white/5"; -export function DocumentUploadTab({ searchSpaceId, onSuccess }: DocumentUploadTabProps) { +export function DocumentUploadTab({ searchSpaceId, onSuccess, onAccordionStateChange }: DocumentUploadTabProps) { const t = useTranslations("upload_documents"); const router = useRouter(); const [files, setFiles] = useState([]); const [uploadProgress, setUploadProgress] = useState(0); + const [accordionValue, setAccordionValue] = useState(""); const [uploadDocumentMutation] = useAtom(uploadDocumentMutationAtom); const { mutate: uploadDocuments, isPending: isUploading } = uploadDocumentMutation; const fileInputRef = useRef(null); @@ -154,6 +156,12 @@ export function DocumentUploadTab({ searchSpaceId, onSuccess }: DocumentUploadTa const totalFileSize = files.reduce((total, file) => total + file.size, 0); + // Track accordion state changes + const handleAccordionChange = useCallback((value: string) => { + setAccordionValue(value); + onAccordionStateChange?.(value === "supported-file-types"); + }, [onAccordionStateChange]); + const handleUpload = async () => { setUploadProgress(0); trackDocumentUploadStarted(Number(searchSpaceId), files.length, totalFileSize); @@ -190,11 +198,11 @@ export function DocumentUploadTab({ searchSpaceId, onSuccess }: DocumentUploadTa initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.3 }} - className="space-y-3 sm:space-y-6 max-w-4xl mx-auto" + className="space-y-3 sm:space-y-6 max-w-4xl mx-auto pt-0" > - - - {t("file_size_limit")} + + + {t("file_size_limit")} @@ -366,11 +374,13 @@ export function DocumentUploadTab({ searchSpaceId, onSuccess }: DocumentUploadTa - -
+ +
From d576607d67049b78b1642af291f6ee66d2efb1fe Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Fri, 2 Jan 2026 17:17:49 +0530 Subject: [PATCH 02/75] refactor: improve connector popup UI and date formatting - Adjusted the height of the dialog content in the connector popup for better layout. - Enhanced the last indexed date display with a new function for contextual formatting, providing clearer time references. - Updated various text sizes for consistency across the connector card and dialog header components. - Minor layout adjustments in the connector dialog header and active connectors tab for improved spacing. --- .../assistant-ui/connector-popup.tsx | 4 +- .../components/connector-card.tsx | 57 +++++++++++++++++-- .../components/connector-dialog-header.tsx | 8 +-- .../tabs/active-connectors-tab.tsx | 50 ++++++++++++++-- 4 files changed, 103 insertions(+), 16 deletions(-) diff --git a/surfsense_web/components/assistant-ui/connector-popup.tsx b/surfsense_web/components/assistant-ui/connector-popup.tsx index a955e3972..8fb1e7652 100644 --- a/surfsense_web/components/assistant-ui/connector-popup.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup.tsx @@ -189,7 +189,7 @@ export const ConnectorIndicator: FC = () => { )} - + {/* YouTube Crawler View - shown when adding YouTube videos */} {isYouTubeView && searchSpaceId ? ( @@ -272,7 +272,7 @@ export const ConnectorIndicator: FC = () => { {/* Content */}
-
+
= ({ id, title, @@ -86,13 +131,13 @@ export const ConnectorCard: FC = ({ // Show last indexed date for connected connectors if (lastIndexedAt) { return ( - - Last indexed: {format(new Date(lastIndexedAt), "MMM d, yyyy")} + + Last indexed: {formatLastIndexedDate(lastIndexedAt)} ); } // Fallback for connected but never indexed - return Never indexed; + return Never indexed; } return description; @@ -113,9 +158,9 @@ export const ConnectorCard: FC = ({
{title}
-
{getStatusContent()}
+
{getStatusContent()}
{isConnected && documentCount !== undefined && ( -

+

{formatDocumentCount(documentCount)}

)} diff --git a/surfsense_web/components/assistant-ui/connector-popup/components/connector-dialog-header.tsx b/surfsense_web/components/assistant-ui/connector-popup/components/connector-dialog-header.tsx index a18c79a1f..f1a06de81 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/components/connector-dialog-header.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/components/connector-dialog-header.tsx @@ -24,20 +24,20 @@ export const ConnectorDialogHeader: FC = ({ return (
- + Connectors - + Search across all your apps and data in one place. -
+
= ({ return `${m.replace(/\.0$/, "")}M docs`; }; + // Format last indexed date with contextual messages + const formatLastIndexedDate = (dateString: string): string => { + const date = new Date(dateString); + const now = new Date(); + const minutesAgo = differenceInMinutes(now, date); + const daysAgo = differenceInDays(now, date); + + // Just now (within last minute) + if (minutesAgo < 1) { + return "Just now"; + } + + // X minutes ago (less than 1 hour) + if (minutesAgo < 60) { + return `${minutesAgo} ${minutesAgo === 1 ? "minute" : "minutes"} ago`; + } + + // Today at [time] + if (isToday(date)) { + return `Today at ${format(date, "h:mm a")}`; + } + + // Yesterday at [time] + if (isYesterday(date)) { + return `Yesterday at ${format(date, "h:mm a")}`; + } + + // X days ago (less than 7 days) + if (daysAgo < 7) { + return `${daysAgo} ${daysAgo === 1 ? "day" : "days"} ago`; + } + + // Full date for older entries + return format(date, "MMM d, yyyy"); + }; + // Document types that should be shown as cards (not from connectors) // These are: EXTENSION (browser extension), FILE (uploaded files), NOTE (editor notes), // YOUTUBE_VIDEO (YouTube videos), and CRAWLED_URL (web pages - shown separately even though it can come from WEBCRAWLER_CONNECTOR) @@ -148,13 +190,13 @@ export const ActiveConnectorsTab: FC = ({ )}

) : ( -

+

{connector.last_indexed_at - ? `Last indexed: ${format(new Date(connector.last_indexed_at), "MMM d, yyyy")}` + ? `Last indexed: ${formatLastIndexedDate(connector.last_indexed_at)}` : "Never indexed"}

)} -

+

{formatDocumentCount(documentCount)}

From 2b01120c2b7e587fd74fb8713465371f27e0a2f6 Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Fri, 2 Jan 2026 18:22:38 +0530 Subject: [PATCH 03/75] refactor: disable write_todos functionality across chat and UI components - Commented out the write_todos tracking and messaging logic in the stream_new_chat.py file. - Disabled the import and usage of WriteTodosToolUI in the new-chat page component. - Updated related logic in the active connectors tab to remove indexing state handling for write_todos. - These changes are part of a temporary disablement of the write_todos feature for further evaluation. --- .../app/tasks/chat/stream_new_chat.py | 269 +++++++++--------- .../new-chat/[[...chat_id]]/page.tsx | 17 +- .../components/connector-card.tsx | 4 +- .../tabs/active-connectors-tab.tsx | 3 +- 4 files changed, 147 insertions(+), 146 deletions(-) diff --git a/surfsense_backend/app/tasks/chat/stream_new_chat.py b/surfsense_backend/app/tasks/chat/stream_new_chat.py index 69b75e5c4..3b87c33f1 100644 --- a/surfsense_backend/app/tasks/chat/stream_new_chat.py +++ b/surfsense_backend/app/tasks/chat/stream_new_chat.py @@ -270,7 +270,8 @@ async def stream_new_chat( # Track if we just finished a tool (text flows silently after tools) just_finished_tool: bool = False # Track write_todos calls to show "Creating plan" vs "Updating plan" - write_todos_call_count: int = 0 + # Disabled for now + # write_todos_call_count: int = 0 def next_thinking_step_id() -> str: nonlocal thinking_step_counter @@ -479,60 +480,60 @@ async def stream_new_chat( status="in_progress", items=last_active_step_items, ) - elif tool_name == "write_todos": - # Track write_todos calls for better messaging - write_todos_call_count += 1 - todos = ( - tool_input.get("todos", []) - if isinstance(tool_input, dict) - else [] - ) - todo_count = len(todos) if isinstance(todos, list) else 0 + # elif tool_name == "write_todos": # Disabled for now + # # Track write_todos calls for better messaging + # write_todos_call_count += 1 + # todos = ( + # tool_input.get("todos", []) + # if isinstance(tool_input, dict) + # else [] + # ) + # todo_count = len(todos) if isinstance(todos, list) else 0 - if write_todos_call_count == 1: - # First call - creating the plan - last_active_step_title = "Creating plan" - last_active_step_items = [f"Defining {todo_count} tasks..."] - else: - # Subsequent calls - updating the plan - # Try to provide context about what's being updated - in_progress_count = ( - sum( - 1 - for t in todos - if isinstance(t, dict) - and t.get("status") == "in_progress" - ) - if isinstance(todos, list) - else 0 - ) - completed_count = ( - sum( - 1 - for t in todos - if isinstance(t, dict) - and t.get("status") == "completed" - ) - if isinstance(todos, list) - else 0 - ) + # if write_todos_call_count == 1: + # # First call - creating the plan + # last_active_step_title = "Creating plan" + # last_active_step_items = [f"Defining {todo_count} tasks..."] + # else: + # # Subsequent calls - updating the plan + # # Try to provide context about what's being updated + # in_progress_count = ( + # sum( + # 1 + # for t in todos + # if isinstance(t, dict) + # and t.get("status") == "in_progress" + # ) + # if isinstance(todos, list) + # else 0 + # ) + # completed_count = ( + # sum( + # 1 + # for t in todos + # if isinstance(t, dict) + # and t.get("status") == "completed" + # ) + # if isinstance(todos, list) + # else 0 + # ) - last_active_step_title = "Updating progress" - last_active_step_items = ( - [ - f"Progress: {completed_count}/{todo_count} completed", - f"In progress: {in_progress_count} tasks", - ] - if completed_count > 0 - else [f"Working on {todo_count} tasks"] - ) + # last_active_step_title = "Updating progress" + # last_active_step_items = ( + # [ + # f"Progress: {completed_count}/{todo_count} completed", + # f"In progress: {in_progress_count} tasks", + # ] + # if completed_count > 0 + # else [f"Working on {todo_count} tasks"] + # ) - yield streaming_service.format_thinking_step( - step_id=tool_step_id, - title=last_active_step_title, - status="in_progress", - items=last_active_step_items, - ) + # yield streaming_service.format_thinking_step( + # step_id=tool_step_id, + # title=last_active_step_title, + # status="in_progress", + # items=last_active_step_items, + # ) elif tool_name == "generate_podcast": podcast_title = ( tool_input.get("podcast_title", "SurfSense Podcast") @@ -596,10 +597,12 @@ async def stream_new_chat( raw_output = event.get("data", {}).get("output", "") # Handle deepagents' write_todos Command object specially - if tool_name == "write_todos" and hasattr(raw_output, "update"): - # deepagents returns a Command object - extract todos directly - tool_output = extract_todos_from_deepagents(raw_output) - elif hasattr(raw_output, "content"): + # Disabled for now + # if tool_name == "write_todos" and hasattr(raw_output, "update"): + # # deepagents returns a Command object - extract todos directly + # tool_output = extract_todos_from_deepagents(raw_output) + # elif hasattr(raw_output, "content"): + if hasattr(raw_output, "content"): # It's a ToolMessage object - extract the content content = raw_output.content # If content is a string that looks like JSON, try to parse it @@ -758,63 +761,63 @@ async def stream_new_chat( status="completed", items=completed_items, ) - elif tool_name == "write_todos": - # Build completion items for planning/updating - if isinstance(tool_output, dict): - todos = tool_output.get("todos", []) - todo_count = len(todos) if isinstance(todos, list) else 0 - completed_count = ( - sum( - 1 - for t in todos - if isinstance(t, dict) - and t.get("status") == "completed" - ) - if isinstance(todos, list) - else 0 - ) - in_progress_count = ( - sum( - 1 - for t in todos - if isinstance(t, dict) - and t.get("status") == "in_progress" - ) - if isinstance(todos, list) - else 0 - ) + # elif tool_name == "write_todos": # Disabled for now + # # Build completion items for planning/updating + # if isinstance(tool_output, dict): + # todos = tool_output.get("todos", []) + # todo_count = len(todos) if isinstance(todos, list) else 0 + # completed_count = ( + # sum( + # 1 + # for t in todos + # if isinstance(t, dict) + # and t.get("status") == "completed" + # ) + # if isinstance(todos, list) + # else 0 + # ) + # in_progress_count = ( + # sum( + # 1 + # for t in todos + # if isinstance(t, dict) + # and t.get("status") == "in_progress" + # ) + # if isinstance(todos, list) + # else 0 + # ) - # Use context-aware completion message - if last_active_step_title == "Creating plan": - completed_items = [f"Created {todo_count} tasks"] - else: - # Updating progress - show stats - completed_items = [ - f"Progress: {completed_count}/{todo_count} completed", - ] - if in_progress_count > 0: - # Find the currently in-progress task name - in_progress_task = next( - ( - t.get("content", "")[:40] - for t in todos - if isinstance(t, dict) - and t.get("status") == "in_progress" - ), - None, - ) - if in_progress_task: - completed_items.append( - f"Current: {in_progress_task}..." - ) - else: - completed_items = ["Plan updated"] - yield streaming_service.format_thinking_step( - step_id=original_step_id, - title=last_active_step_title, - status="completed", - items=completed_items, - ) + # # Use context-aware completion message + # if last_active_step_title == "Creating plan": + # completed_items = [f"Created {todo_count} tasks"] + # else: + # # Updating progress - show stats + # completed_items = [ + # f"Progress: {completed_count}/{todo_count} completed", + # ] + # if in_progress_count > 0: + # # Find the currently in-progress task name + # in_progress_task = next( + # ( + # t.get("content", "")[:40] + # for t in todos + # if isinstance(t, dict) + # and t.get("status") == "in_progress" + # ), + # None, + # ) + # if in_progress_task: + # completed_items.append( + # f"Current: {in_progress_task}..." + # ) + # else: + # completed_items = ["Plan updated"] + # yield streaming_service.format_thinking_step( + # step_id=original_step_id, + # title=last_active_step_title, + # status="completed", + # items=completed_items, + # ) elif tool_name == "ls": # Build completion items showing file names found if isinstance(tool_output, dict): @@ -992,27 +995,27 @@ async def stream_new_chat( yield streaming_service.format_terminal_info( "Knowledge base search completed", "success" ) - elif tool_name == "write_todos": - # Stream the full write_todos result so frontend can render the Plan component - yield streaming_service.format_tool_output_available( - tool_call_id, - tool_output - if isinstance(tool_output, dict) - else {"result": tool_output}, - ) - # Send terminal message with plan info - if isinstance(tool_output, dict): - todos = tool_output.get("todos", []) - todo_count = len(todos) if isinstance(todos, list) else 0 - yield streaming_service.format_terminal_info( - f"Plan created ({todo_count} tasks)", - "success", - ) - else: - yield streaming_service.format_terminal_info( - "Plan created", - "success", - ) + # elif tool_name == "write_todos": # Disabled for now + # # Stream the full write_todos result so frontend can render the Plan component + # yield streaming_service.format_tool_output_available( + # tool_call_id, + # tool_output + # if isinstance(tool_output, dict) + # else {"result": tool_output}, + # ) + # # Send terminal message with plan info + # if isinstance(tool_output, dict): + # todos = tool_output.get("todos", []) + # todo_count = len(todos) if isinstance(todos, list) else 0 + # yield streaming_service.format_terminal_info( + # f"Plan created ({todo_count} tasks)", + # "success", + # ) + # else: + # yield streaming_service.format_terminal_info( + # "Plan created", + # "success", + # ) else: # Default handling for other tools yield streaming_service.format_tool_output_available( diff --git a/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx index 35a096497..b1abd647f 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx @@ -20,7 +20,7 @@ import { } from "@/atoms/chat/mentioned-documents.atom"; import { clearPlanOwnerRegistry, - extractWriteTodosFromContent, + // extractWriteTodosFromContent, hydratePlanStateAtom, } from "@/atoms/chat/plan-state.atom"; import { Thread } from "@/components/assistant-ui/thread"; @@ -30,7 +30,7 @@ import { DisplayImageToolUI } from "@/components/tool-ui/display-image"; import { GeneratePodcastToolUI } from "@/components/tool-ui/generate-podcast"; import { LinkPreviewToolUI } from "@/components/tool-ui/link-preview"; import { ScrapeWebpageToolUI } from "@/components/tool-ui/scrape-webpage"; -import { WriteTodosToolUI } from "@/components/tool-ui/write-todos"; +// import { WriteTodosToolUI } from "@/components/tool-ui/write-todos"; import { getBearerToken } from "@/lib/auth-utils"; import { createAttachmentAdapter, extractAttachmentContent } from "@/lib/chat/attachment-adapter"; import { @@ -199,7 +199,7 @@ const TOOLS_WITH_UI = new Set([ "link_preview", "display_image", "scrape_webpage", - "write_todos", + // "write_todos", // Disabled for now ]); /** @@ -291,10 +291,11 @@ export default function NewChatPage() { restoredThinkingSteps.set(`msg-${msg.id}`, steps); } // Hydrate write_todos plan state from persisted tool calls - const writeTodosCalls = extractWriteTodosFromContent(msg.content); - for (const todoData of writeTodosCalls) { - hydratePlanState(todoData); - } + // Disabled for now + // const writeTodosCalls = extractWriteTodosFromContent(msg.content); + // for (const todoData of writeTodosCalls) { + // hydratePlanState(todoData); + // } } if (msg.role === "user") { const docs = extractMentionedDocuments(msg.content); @@ -911,7 +912,7 @@ export default function NewChatPage() { - + {/* Disabled for now */}
= ({ !isConnected && "shadow-xs" )} onClick={isConnected ? onManage : onConnect} - disabled={isConnecting || isIndexing} + disabled={isConnecting} > {isConnecting ? ( - ) : isIndexing ? ( - "Syncing..." ) : isConnected ? ( "Manage" ) : id === "youtube-crawler" ? ( diff --git a/surfsense_web/components/assistant-ui/connector-popup/tabs/active-connectors-tab.tsx b/surfsense_web/components/assistant-ui/connector-popup/tabs/active-connectors-tab.tsx index 12e0d4472..ec3177e28 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/tabs/active-connectors-tab.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/tabs/active-connectors-tab.tsx @@ -205,9 +205,8 @@ export const ActiveConnectorsTab: FC = ({ size="sm" className="h-8 text-[11px] px-3 rounded-lg font-medium bg-white text-slate-700 hover:bg-slate-50 border-0 shadow-xs dark:bg-secondary dark:text-secondary-foreground dark:hover:bg-secondary/80" onClick={onManage ? () => onManage(connector) : undefined} - disabled={isIndexing} > - {isIndexing ? "Syncing..." : "Manage"} + Manage
); From c5b184d4758355696b2c8179a47af183c5ee6194 Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Fri, 2 Jan 2026 20:07:14 +0530 Subject: [PATCH 04/75] feat: add Notion OAuth integration and connector routes - Introduced Notion OAuth support with new environment variables for client ID, client secret, and redirect URI. - Implemented Notion connector routes for OAuth flow, including authorization and callback handling. - Updated existing components to accommodate Notion integration, including validation changes and connector configuration. - Enhanced the Notion indexer to utilize OAuth access tokens instead of integration tokens. - Adjusted UI components to reflect the new Notion connector without requiring special configuration. --- surfsense_backend/.env.example | 6 +- surfsense_backend/app/config/__init__.py | 5 + .../app/connectors/notion_history.py | 2 +- surfsense_backend/app/routes/__init__.py | 2 + .../app/routes/notion_add_connector_route.py | 246 ++++++++++++++++++ .../connector_indexers/notion_indexer.py | 59 ++--- surfsense_backend/app/utils/validators.py | 8 +- .../components/circleback-connect-form.tsx | 2 +- .../connector-popup/connect-forms/index.tsx | 3 - .../connector-configs/index.tsx | 5 +- .../views/connector-connect-view.tsx | 1 - .../views/connector-edit-view.tsx | 21 +- .../views/indexing-configuration-view.tsx | 34 ++- .../constants/connector-constants.ts | 15 +- 14 files changed, 333 insertions(+), 76 deletions(-) create mode 100644 surfsense_backend/app/routes/notion_add_connector_route.py diff --git a/surfsense_backend/.env.example b/surfsense_backend/.env.example index 91a0cb42f..3ee063e15 100644 --- a/surfsense_backend/.env.example +++ b/surfsense_backend/.env.example @@ -38,7 +38,11 @@ GOOGLE_OAUTH_CLIENT_SECRET=GOCSV GOOGLE_CALENDAR_REDIRECT_URI=http://localhost:8000/api/v1/auth/google/calendar/connector/callback GOOGLE_GMAIL_REDIRECT_URI=http://localhost:8000/api/v1/auth/google/gmail/connector/callback GOOGLE_DRIVE_REDIRECT_URI=http://localhost:8000/api/v1/auth/google/drive/connector/callback -GOOGLE_DRIVE_REDIRECT_URI=http://localhost:8000/api/v1/auth/google/drive/connector/callback + +# Notion OAuth for Notion Connector +NOTION_CLIENT_ID=your_notion_client_id +NOTION_CLIENT_SECRET=your_notion_client_secret +NOTION_REDIRECT_URI=http://localhost:8000/api/v1/auth/notion/connector/callback # Airtable OAuth for Aitable Connector AIRTABLE_CLIENT_ID=your_airtable_client_id diff --git a/surfsense_backend/app/config/__init__.py b/surfsense_backend/app/config/__init__.py index 9c503fb18..61e150bf3 100644 --- a/surfsense_backend/app/config/__init__.py +++ b/surfsense_backend/app/config/__init__.py @@ -90,6 +90,11 @@ class Config: AIRTABLE_CLIENT_SECRET = os.getenv("AIRTABLE_CLIENT_SECRET") AIRTABLE_REDIRECT_URI = os.getenv("AIRTABLE_REDIRECT_URI") + # Notion OAuth + NOTION_CLIENT_ID = os.getenv("NOTION_CLIENT_ID") + NOTION_CLIENT_SECRET = os.getenv("NOTION_CLIENT_SECRET") + NOTION_REDIRECT_URI = os.getenv("NOTION_REDIRECT_URI") + # LLM instances are now managed per-user through the LLMConfig system # Legacy environment variables removed in favor of user-specific configurations diff --git a/surfsense_backend/app/connectors/notion_history.py b/surfsense_backend/app/connectors/notion_history.py index 81f6642f1..1f6300575 100644 --- a/surfsense_backend/app/connectors/notion_history.py +++ b/surfsense_backend/app/connectors/notion_history.py @@ -7,7 +7,7 @@ class NotionHistoryConnector: Initialize the NotionPageFetcher with a token. Args: - token (str): Notion integration token + token (str): Notion OAuth access token """ self.notion = AsyncClient(auth=token) diff --git a/surfsense_backend/app/routes/__init__.py b/surfsense_backend/app/routes/__init__.py index 3c18650ae..1246dfe39 100644 --- a/surfsense_backend/app/routes/__init__.py +++ b/surfsense_backend/app/routes/__init__.py @@ -18,6 +18,7 @@ from .google_gmail_add_connector_route import ( from .logs_routes import router as logs_router from .luma_add_connector_route import router as luma_add_connector_router from .new_chat_routes import router as new_chat_router +from .notion_add_connector_route import router as notion_add_connector_router from .new_llm_config_routes import router as new_llm_config_router from .notes_routes import router as notes_router from .podcasts_routes import router as podcasts_router @@ -40,6 +41,7 @@ router.include_router(google_gmail_add_connector_router) router.include_router(google_drive_add_connector_router) router.include_router(airtable_add_connector_router) router.include_router(luma_add_connector_router) +router.include_router(notion_add_connector_router) router.include_router(new_llm_config_router) # LLM configs with prompt configuration router.include_router(logs_router) router.include_router(circleback_webhook_router) # Circleback meeting webhooks diff --git a/surfsense_backend/app/routes/notion_add_connector_route.py b/surfsense_backend/app/routes/notion_add_connector_route.py new file mode 100644 index 000000000..38c435ff1 --- /dev/null +++ b/surfsense_backend/app/routes/notion_add_connector_route.py @@ -0,0 +1,246 @@ +""" +Notion Connector OAuth Routes. + +Handles OAuth 2.0 authentication flow for Notion connector. +""" + +import base64 +import json +import logging +from uuid import UUID + +import httpx +from fastapi import APIRouter, Depends, HTTPException, Request +from fastapi.responses import RedirectResponse +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, + SearchSourceConnectorType, + User, + get_async_session, +) +from app.users import current_active_user + +logger = logging.getLogger(__name__) + +router = APIRouter() + +# Notion OAuth endpoints +AUTHORIZATION_URL = "https://api.notion.com/v1/oauth/authorize" +TOKEN_URL = "https://api.notion.com/v1/oauth/token" + + +def make_basic_auth_header(client_id: str, client_secret: str) -> str: + """Create Basic Auth header for Notion OAuth.""" + credentials = f"{client_id}:{client_secret}".encode() + b64 = base64.b64encode(credentials).decode("ascii") + return f"Basic {b64}" + + +@router.get("/auth/notion/connector/add") +async def connect_notion(space_id: int, user: User = Depends(current_active_user)): + """ + Initiate Notion OAuth flow. + + Args: + space_id: The search space ID + user: Current authenticated user + + Returns: + Authorization URL for redirect + """ + try: + if not space_id: + raise HTTPException(status_code=400, detail="space_id is required") + + if not config.NOTION_CLIENT_ID: + raise HTTPException( + status_code=500, detail="Notion OAuth not configured." + ) + + # Generate state parameter + state_payload = json.dumps( + { + "space_id": space_id, + "user_id": str(user.id), + } + ) + state_encoded = base64.urlsafe_b64encode(state_payload.encode()).decode() + + # Build authorization URL + from urllib.parse import urlencode + + auth_params = { + "client_id": config.NOTION_CLIENT_ID, + "response_type": "code", + "owner": "user", # Allows both admins and members to authorize + "redirect_uri": config.NOTION_REDIRECT_URI, + "state": state_encoded, + } + + auth_url = f"{AUTHORIZATION_URL}?{urlencode(auth_params)}" + + logger.info( + f"Generated Notion OAuth URL for user {user.id}, space {space_id}" + ) + return {"auth_url": auth_url} + + except Exception as e: + logger.error(f"Failed to initiate Notion OAuth: {e!s}", exc_info=True) + raise HTTPException( + status_code=500, detail=f"Failed to initiate Notion OAuth: {e!s}" + ) from e + + +@router.get("/auth/notion/connector/callback") +async def notion_callback( + request: Request, + code: str, + state: str, + session: AsyncSession = Depends(get_async_session), +): + """ + Handle Notion OAuth callback. + + Args: + request: FastAPI request object + code: Authorization code from Notion + state: State parameter containing user/space info + session: Database session + + Returns: + Redirect response to frontend + """ + try: + # Decode and parse the state + try: + decoded_state = base64.urlsafe_b64decode(state.encode()).decode() + data = json.loads(decoded_state) + except Exception as e: + raise HTTPException( + status_code=400, detail=f"Invalid state parameter: {e!s}" + ) from e + + user_id = UUID(data["user_id"]) + space_id = data["space_id"] + + # Exchange authorization code for access token + auth_header = make_basic_auth_header( + config.NOTION_CLIENT_ID, config.NOTION_CLIENT_SECRET + ) + + token_data = { + "grant_type": "authorization_code", + "code": code, + "redirect_uri": config.NOTION_REDIRECT_URI, + } + + async with httpx.AsyncClient() as client: + token_response = await client.post( + TOKEN_URL, + json=token_data, + headers={ + "Content-Type": "application/json", + "Authorization": auth_header, + }, + timeout=30.0, + ) + + if token_response.status_code != 200: + error_detail = token_response.text + try: + error_json = token_response.json() + error_detail = error_json.get("error_description", error_detail) + except Exception: + pass + raise HTTPException( + status_code=400, detail=f"Token exchange failed: {error_detail}" + ) + + token_json = token_response.json() + + # Notion returns access_token and workspace information + # Store the access token and workspace info in connector config + connector_config = { + "access_token": token_json["access_token"], + "workspace_id": token_json.get("workspace_id"), + "workspace_name": token_json.get("workspace_name"), + "workspace_icon": token_json.get("workspace_icon"), + "bot_id": token_json.get("bot_id"), + } + + # Check if connector already exists for this search space and user + existing_connector_result = await session.execute( + select(SearchSourceConnector).filter( + SearchSourceConnector.search_space_id == space_id, + SearchSourceConnector.user_id == user_id, + SearchSourceConnector.connector_type + == SearchSourceConnectorType.NOTION_CONNECTOR, + ) + ) + existing_connector = existing_connector_result.scalars().first() + + if existing_connector: + # Update existing connector + existing_connector.config = connector_config + existing_connector.name = "Notion Connector" + existing_connector.is_indexable = True + logger.info( + f"Updated existing Notion connector for user {user_id} in space {space_id}" + ) + else: + # Create new connector + new_connector = SearchSourceConnector( + name="Notion Connector", + connector_type=SearchSourceConnectorType.NOTION_CONNECTOR, + is_indexable=True, + config=connector_config, + search_space_id=space_id, + user_id=user_id, + ) + session.add(new_connector) + logger.info( + f"Created new Notion connector for user {user_id} in space {space_id}" + ) + + try: + await session.commit() + logger.info(f"Successfully saved Notion connector for user {user_id}") + + # Redirect to the frontend with success params + return RedirectResponse( + url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&success=true&connector=notion-connector" + ) + + except ValidationError as e: + await session.rollback() + raise HTTPException( + status_code=422, detail=f"Validation error: {e!s}" + ) from e + except IntegrityError as e: + await session.rollback() + raise HTTPException( + status_code=409, + detail=f"Integrity error: A connector with this type already exists. {e!s}", + ) from e + except Exception as e: + logger.error(f"Failed to create search source connector: {e!s}") + await session.rollback() + raise HTTPException( + status_code=500, + detail=f"Failed to create search source connector: {e!s}", + ) from e + + except HTTPException: + raise + except Exception as e: + logger.error(f"Failed to complete Notion OAuth: {e!s}", exc_info=True) + raise HTTPException( + status_code=500, detail=f"Failed to complete Notion OAuth: {e!s}" + ) from e + diff --git a/surfsense_backend/app/tasks/connector_indexers/notion_indexer.py b/surfsense_backend/app/tasks/connector_indexers/notion_indexer.py index 332d3e39d..b42626667 100644 --- a/surfsense_backend/app/tasks/connector_indexers/notion_indexer.py +++ b/surfsense_backend/app/tasks/connector_indexers/notion_indexer.py @@ -2,7 +2,7 @@ Notion connector indexer. """ -from datetime import datetime, timedelta +from datetime import datetime from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.ext.asyncio import AsyncSession @@ -20,6 +20,7 @@ from app.utils.document_converters import ( from .base import ( build_document_metadata_string, + calculate_date_range, check_document_by_unique_identifier, get_connector_by_id, get_current_timestamp, @@ -91,16 +92,16 @@ async def index_notion_pages( f"Connector with ID {connector_id} not found or is not a Notion connector", ) - # Get the Notion token from the connector config - notion_token = connector.config.get("NOTION_INTEGRATION_TOKEN") + # Get the Notion access token from the connector config (OAuth-based) + notion_token = connector.config.get("access_token") if not notion_token: await task_logger.log_task_failure( log_entry, - f"Notion integration token not found in connector config for connector {connector_id}", - "Missing Notion token", + f"Notion access token not found in connector config for connector {connector_id}", + "Missing Notion access token", {"error_type": "MissingToken"}, ) - return 0, "Notion integration token not found in connector config" + return 0, "Notion access token not found in connector config" # Initialize Notion client await task_logger.log_task_progress( @@ -111,38 +112,24 @@ async def index_notion_pages( logger.info(f"Initializing Notion client for connector {connector_id}") - # Calculate date range - if start_date is None or end_date is None: - # Fall back to calculating dates - calculated_end_date = datetime.now() - calculated_start_date = calculated_end_date - timedelta( - days=365 - ) # Check for last 1 year of pages + # Handle 'undefined' string from frontend (treat as None) + if start_date == "undefined" or start_date == "": + start_date = None + if end_date == "undefined" or end_date == "": + end_date = None - # Use calculated dates if not provided - if start_date is None: - start_date_iso = calculated_start_date.strftime("%Y-%m-%dT%H:%M:%SZ") - else: - # Convert YYYY-MM-DD to ISO format - start_date_iso = datetime.strptime(start_date, "%Y-%m-%d").strftime( - "%Y-%m-%dT%H:%M:%SZ" - ) + # Calculate date range using the shared utility function + start_date_str, end_date_str = calculate_date_range( + connector, start_date, end_date, default_days_back=365 + ) - if end_date is None: - end_date_iso = calculated_end_date.strftime("%Y-%m-%dT%H:%M:%SZ") - else: - # Convert YYYY-MM-DD to ISO format - end_date_iso = datetime.strptime(end_date, "%Y-%m-%d").strftime( - "%Y-%m-%dT%H:%M:%SZ" - ) - else: - # Convert provided dates to ISO format for Notion API - start_date_iso = datetime.strptime(start_date, "%Y-%m-%d").strftime( - "%Y-%m-%dT%H:%M:%SZ" - ) - end_date_iso = datetime.strptime(end_date, "%Y-%m-%d").strftime( - "%Y-%m-%dT%H:%M:%SZ" - ) + # Convert YYYY-MM-DD to ISO format for Notion API + start_date_iso = datetime.strptime(start_date_str, "%Y-%m-%d").strftime( + "%Y-%m-%dT%H:%M:%SZ" + ) + end_date_iso = datetime.strptime(end_date_str, "%Y-%m-%d").strftime( + "%Y-%m-%dT%H:%M:%SZ" + ) notion_client = NotionHistoryConnector(token=notion_token) diff --git a/surfsense_backend/app/utils/validators.py b/surfsense_backend/app/utils/validators.py index 6b69fb3e1..1e76afc67 100644 --- a/surfsense_backend/app/utils/validators.py +++ b/surfsense_backend/app/utils/validators.py @@ -515,7 +515,13 @@ def validate_connector_config( }, "SLACK_CONNECTOR": {"required": ["SLACK_BOT_TOKEN"], "validators": {}}, "NOTION_CONNECTOR": { - "required": ["NOTION_INTEGRATION_TOKEN"], + "required": ["access_token"], # OAuth-based only + "optional": [ + "workspace_id", # OAuth fields + "workspace_name", + "workspace_icon", + "bot_id", + ], "validators": {}, }, "GITHUB_CONNECTOR": { diff --git a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/circleback-connect-form.tsx b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/circleback-connect-form.tsx index 75a3ab00b..cd7f1a888 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/circleback-connect-form.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/circleback-connect-form.tsx @@ -1,7 +1,7 @@ "use client"; import { zodResolver } from "@hookform/resolvers/zod"; -import { Info, Webhook } from "lucide-react"; +import { Webhook } from "lucide-react"; import type { FC } from "react"; import { useRef } from "react"; import { useForm } from "react-hook-form"; diff --git a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/index.tsx b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/index.tsx index 7bca3a1bc..e84cb3b96 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/index.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/index.tsx @@ -11,7 +11,6 @@ import { JiraConnectForm } from "./components/jira-connect-form"; import { LinearConnectForm } from "./components/linear-connect-form"; import { LinkupApiConnectForm } from "./components/linkup-api-connect-form"; import { LumaConnectForm } from "./components/luma-connect-form"; -import { NotionConnectForm } from "./components/notion-connect-form"; import { SearxngConnectForm } from "./components/searxng-connect-form"; import { SlackConnectForm } from "./components/slack-connect-form"; import { TavilyApiConnectForm } from "./components/tavily-api-connect-form"; @@ -59,8 +58,6 @@ export function getConnectFormComponent(connectorType: string): ConnectFormCompo return SlackConnectForm; case "DISCORD_CONNECTOR": return DiscordConnectForm; - case "NOTION_CONNECTOR": - return NotionConnectForm; case "CONFLUENCE_CONNECTOR": return ConfluenceConnectForm; case "BOOKSTACK_CONNECTOR": diff --git a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/index.tsx b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/index.tsx index c31a4645a..793b961e3 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/index.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/index.tsx @@ -15,7 +15,6 @@ import { JiraConfig } from "./components/jira-config"; import { LinearConfig } from "./components/linear-config"; import { LinkupApiConfig } from "./components/linkup-api-config"; import { LumaConfig } from "./components/luma-config"; -import { NotionConfig } from "./components/notion-config"; import { SearxngConfig } from "./components/searxng-config"; import { SlackConfig } from "./components/slack-config"; import { TavilyApiConfig } from "./components/tavily-api-config"; @@ -56,8 +55,6 @@ export function getConnectorConfigComponent( return SlackConfig; case "DISCORD_CONNECTOR": return DiscordConfig; - case "NOTION_CONNECTOR": - return NotionConfig; case "CONFLUENCE_CONNECTOR": return ConfluenceConfig; case "BOOKSTACK_CONNECTOR": @@ -72,7 +69,7 @@ export function getConnectorConfigComponent( return LumaConfig; case "CIRCLEBACK_CONNECTOR": return CirclebackConfig; - // OAuth connectors (Gmail, Calendar, Airtable) and others don't need special config UI + // OAuth connectors (Gmail, Calendar, Airtable, Notion) and others don't need special config UI default: return null; } diff --git a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-connect-view.tsx b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-connect-view.tsx index dfd91fe8b..fd0f62fa0 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-connect-view.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-connect-view.tsx @@ -55,7 +55,6 @@ export const ConnectorConnectView: FC = ({ ELASTICSEARCH_CONNECTOR: "elasticsearch-connect-form", SLACK_CONNECTOR: "slack-connect-form", DISCORD_CONNECTOR: "discord-connect-form", - NOTION_CONNECTOR: "notion-connect-form", CONFLUENCE_CONNECTOR: "confluence-connect-form", BOOKSTACK_CONNECTOR: "bookstack-connect-form", GITHUB_CONNECTOR: "github-connect-form", diff --git a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-edit-view.tsx b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-edit-view.tsx index 6d43e6ffc..7776c9a9d 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-edit-view.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-edit-view.tsx @@ -59,6 +59,7 @@ export const ConnectorEditView: FC = ({ const [isScrolled, setIsScrolled] = useState(false); const [hasMoreContent, setHasMoreContent] = useState(false); const [showDisconnectConfirm, setShowDisconnectConfirm] = useState(false); + const [isQuickIndexing, setIsQuickIndexing] = useState(false); const scrollContainerRef = useRef(null); const checkScrollState = useCallback(() => { @@ -94,6 +95,13 @@ export const ConnectorEditView: FC = ({ }; }, [checkScrollState]); + // Reset local quick indexing state when indexing completes + useEffect(() => { + if (!isIndexing) { + setIsQuickIndexing(false); + } + }, [isIndexing]); + const handleDisconnectClick = () => { setShowDisconnectConfirm(true); }; @@ -107,6 +115,13 @@ export const ConnectorEditView: FC = ({ setShowDisconnectConfirm(false); }; + const handleQuickIndex = useCallback(() => { + if (onQuickIndex) { + setIsQuickIndexing(true); + onQuickIndex(); + } + }, [onQuickIndex]); + return (
{/* Fixed Header */} @@ -146,11 +161,11 @@ export const ConnectorEditView: FC = ({ + {/* Back button - only show if not from OAuth */} + {!isFromOAuth && ( + + )} {/* Success header */}
@@ -187,15 +193,7 @@ export const IndexingConfigurationView: FC = ({
{/* Fixed Footer - Action buttons */} -
- +
); })} + {/* Loading indicator for additional pages */} + {isLoadingMore && ( +
+
+
+ )}
)}
From 431ea44b5691fb39e661f7cf71763df3016a3c71 Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Sat, 3 Jan 2026 23:06:09 +0530 Subject: [PATCH 13/75] feat: enhance periodic indexing configuration with detailed validation and UI options - Updated the SearchSourceConnectorBase class to include detailed documentation on supported periodic indexing frequencies. - Added "Every 5 minutes" option to the frequency selection in multiple connector forms (BookStack, ClickUp, Confluence, Discord, Elasticsearch, Github, Jira, Luma, Slack) to improve user experience and flexibility in scheduling. --- surfsense_backend/app/schemas/search_source_connector.py | 7 ++++++- .../connector-popup/components/periodic-sync-config.tsx | 3 +++ .../connect-forms/components/bookstack-connect-form.tsx | 3 +++ .../connect-forms/components/clickup-connect-form.tsx | 3 +++ .../connect-forms/components/confluence-connect-form.tsx | 3 +++ .../connect-forms/components/discord-connect-form.tsx | 3 +++ .../components/elasticsearch-connect-form.tsx | 3 +++ .../connect-forms/components/github-connect-form.tsx | 3 +++ .../connect-forms/components/jira-connect-form.tsx | 3 +++ .../connect-forms/components/luma-connect-form.tsx | 3 +++ .../connect-forms/components/slack-connect-form.tsx | 3 +++ .../connector-popup/constants/connector-popup.schemas.ts | 2 +- 12 files changed, 37 insertions(+), 2 deletions(-) diff --git a/surfsense_backend/app/schemas/search_source_connector.py b/surfsense_backend/app/schemas/search_source_connector.py index 1e8a7a38d..b0d8ebc3a 100644 --- a/surfsense_backend/app/schemas/search_source_connector.py +++ b/surfsense_backend/app/schemas/search_source_connector.py @@ -30,7 +30,12 @@ class SearchSourceConnectorBase(BaseModel): @model_validator(mode="after") def validate_periodic_indexing(self): - """Validate that periodic indexing configuration is consistent.""" + """Validate that periodic indexing configuration is consistent. + + Supported frequencies: Any positive integer (in minutes). + Common values: 5, 15, 60 (1 hour), 360 (6 hours), 720 (12 hours), 1440 (daily), etc. + The schedule checker will handle any frequency >= 1 minute. + """ if self.periodic_indexing_enabled: if not self.is_indexable: raise ValueError( diff --git a/surfsense_web/components/assistant-ui/connector-popup/components/periodic-sync-config.tsx b/surfsense_web/components/assistant-ui/connector-popup/components/periodic-sync-config.tsx index 0e1be72b8..f390b1d1b 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/components/periodic-sync-config.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/components/periodic-sync-config.tsx @@ -50,6 +50,9 @@ export const PeriodicSyncConfig: FC = ({ + + Every 5 minutes + Every 15 minutes diff --git a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/bookstack-connect-form.tsx b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/bookstack-connect-form.tsx index b0488854f..2b7123d78 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/bookstack-connect-form.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/bookstack-connect-form.tsx @@ -256,6 +256,9 @@ export const BookStackConnectForm: FC = ({ onSubmit, isSubmitt + + Every 5 minutes + Every 15 minutes diff --git a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/clickup-connect-form.tsx b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/clickup-connect-form.tsx index 5be0045ff..9f33c6ed9 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/clickup-connect-form.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/clickup-connect-form.tsx @@ -209,6 +209,9 @@ export const ClickUpConnectForm: FC = ({ onSubmit, isSubmittin + + Every 5 minutes + Every 15 minutes diff --git a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/confluence-connect-form.tsx b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/confluence-connect-form.tsx index 9f0921bd8..83f6c6ec7 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/confluence-connect-form.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/confluence-connect-form.tsx @@ -263,6 +263,9 @@ export const ConfluenceConnectForm: FC = ({ onSubmit, isSubmit + + Every 5 minutes + Every 15 minutes diff --git a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/discord-connect-form.tsx b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/discord-connect-form.tsx index e0f253129..8f4fa1a47 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/discord-connect-form.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/discord-connect-form.tsx @@ -209,6 +209,9 @@ export const DiscordConnectForm: FC = ({ onSubmit, isSubmittin + + Every 5 minutes + Every 15 minutes diff --git a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/elasticsearch-connect-form.tsx b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/elasticsearch-connect-form.tsx index 24640f7e3..3ceca0930 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/elasticsearch-connect-form.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/elasticsearch-connect-form.tsx @@ -616,6 +616,9 @@ export const ElasticsearchConnectForm: FC = ({ onSubmit, isSub + + Every 5 minutes + Every 15 minutes diff --git a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/github-connect-form.tsx b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/github-connect-form.tsx index 772acb489..b2b371ed8 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/github-connect-form.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/github-connect-form.tsx @@ -269,6 +269,9 @@ export const GithubConnectForm: FC = ({ onSubmit, isSubmitting + + Every 5 minutes + Every 15 minutes diff --git a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/jira-connect-form.tsx b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/jira-connect-form.tsx index d048d9c66..0499554b4 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/jira-connect-form.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/jira-connect-form.tsx @@ -262,6 +262,9 @@ export const JiraConnectForm: FC = ({ onSubmit, isSubmitting } + + Every 5 minutes + Every 15 minutes diff --git a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/luma-connect-form.tsx b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/luma-connect-form.tsx index 2e95fb445..03ab78ddf 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/luma-connect-form.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/luma-connect-form.tsx @@ -209,6 +209,9 @@ export const LumaConnectForm: FC = ({ onSubmit, isSubmitting } + + Every 5 minutes + Every 15 minutes diff --git a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/slack-connect-form.tsx b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/slack-connect-form.tsx index da4605473..3952144e6 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/slack-connect-form.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/slack-connect-form.tsx @@ -211,6 +211,9 @@ export const SlackConnectForm: FC = ({ onSubmit, isSubmitting + + Every 5 minutes + Every 15 minutes diff --git a/surfsense_web/components/assistant-ui/connector-popup/constants/connector-popup.schemas.ts b/surfsense_web/components/assistant-ui/connector-popup/constants/connector-popup.schemas.ts index 3fcdf352f..65456689c 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/constants/connector-popup.schemas.ts +++ b/surfsense_web/components/assistant-ui/connector-popup/constants/connector-popup.schemas.ts @@ -39,7 +39,7 @@ export type IndexingConfigState = z.infer; /** * Schema for frequency minutes (must be one of the allowed values) */ -export const frequencyMinutesSchema = z.enum(["15", "60", "360", "720", "1440", "10080"], { +export const frequencyMinutesSchema = z.enum(["5", "15", "60", "360", "720", "1440", "10080"], { message: "Invalid frequency value", }); From 0fe94bfcf3e77484f594341a41b14eb3cd87e3a1 Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Sun, 4 Jan 2026 02:30:00 +0530 Subject: [PATCH 14/75] feat: add Slack OAuth integration and connector routes - Introduced Slack OAuth support with new environment variables for client ID, client secret, and redirect URI. - Implemented Slack connector routes for OAuth flow, including authorization and callback handling. - Updated configuration to support both new OAuth format and legacy token handling. - Enhanced the Slack indexer to decrypt tokens when necessary, ensuring compatibility with existing encrypted credentials. - Removed outdated Slack connector UI components and adjusted frontend logic to reflect the new integration. --- surfsense_backend/.env.example | 5 + surfsense_backend/app/config/__init__.py | 5 + surfsense_backend/app/routes/__init__.py | 2 + .../app/routes/slack_add_connector_route.py | 336 ++++++++++++++ .../app/schemas/search_source_connector.py | 2 +- .../tasks/connector_indexers/slack_indexer.py | 28 +- surfsense_backend/app/utils/validators.py | 17 +- .../components/slack-connect-form.tsx | 429 ------------------ .../connector-popup/connect-forms/index.tsx | 3 - .../components/slack-config.tsx | 81 +--- .../views/connector-connect-view.tsx | 1 - .../constants/connector-constants.ts | 13 +- 12 files changed, 411 insertions(+), 511 deletions(-) create mode 100644 surfsense_backend/app/routes/slack_add_connector_route.py delete mode 100644 surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/slack-connect-form.tsx diff --git a/surfsense_backend/.env.example b/surfsense_backend/.env.example index 83656a910..2cacedc21 100644 --- a/surfsense_backend/.env.example +++ b/surfsense_backend/.env.example @@ -54,6 +54,11 @@ NOTION_CLIENT_ID=your_notion_client_id NOTION_CLIENT_SECRET=your_notion_client_secret NOTION_REDIRECT_URI=http://localhost:8000/api/v1/auth/notion/connector/callback +# OAuth for Slack connector +SLACK_CLIENT_ID=1234567890.1234567890123 +SLACK_CLIENT_SECRET=abcdefghijklmnopqrstuvwxyz1234567890 +SLACK_REDIRECT_URI=http://localhost:8000/api/v1/auth/slack/connector/callback + # Embedding Model # Examples: # # Get sentence transformers embeddings diff --git a/surfsense_backend/app/config/__init__.py b/surfsense_backend/app/config/__init__.py index 7c7703470..f69d1c1a3 100644 --- a/surfsense_backend/app/config/__init__.py +++ b/surfsense_backend/app/config/__init__.py @@ -100,6 +100,11 @@ class Config: LINEAR_CLIENT_SECRET = os.getenv("LINEAR_CLIENT_SECRET") LINEAR_REDIRECT_URI = os.getenv("LINEAR_REDIRECT_URI") + # Slack OAuth + SLACK_CLIENT_ID = os.getenv("SLACK_CLIENT_ID") + SLACK_CLIENT_SECRET = os.getenv("SLACK_CLIENT_SECRET") + SLACK_REDIRECT_URI = os.getenv("SLACK_REDIRECT_URI") + # LLM instances are now managed per-user through the LLMConfig system # Legacy environment variables removed in favor of user-specific configurations diff --git a/surfsense_backend/app/routes/__init__.py b/surfsense_backend/app/routes/__init__.py index 1d1fa39ad..05020deff 100644 --- a/surfsense_backend/app/routes/__init__.py +++ b/surfsense_backend/app/routes/__init__.py @@ -26,6 +26,7 @@ from .podcasts_routes import router as podcasts_router from .rbac_routes import router as rbac_router from .search_source_connectors_routes import router as search_source_connectors_router from .search_spaces_routes import router as search_spaces_router +from .slack_add_connector_route import router as slack_add_connector_router router = APIRouter() @@ -44,6 +45,7 @@ router.include_router(airtable_add_connector_router) router.include_router(linear_add_connector_router) router.include_router(luma_add_connector_router) router.include_router(notion_add_connector_router) +router.include_router(slack_add_connector_router) router.include_router(new_llm_config_router) # LLM configs with prompt configuration router.include_router(logs_router) router.include_router(circleback_webhook_router) # Circleback meeting webhooks diff --git a/surfsense_backend/app/routes/slack_add_connector_route.py b/surfsense_backend/app/routes/slack_add_connector_route.py new file mode 100644 index 000000000..1bbb4f5f1 --- /dev/null +++ b/surfsense_backend/app/routes/slack_add_connector_route.py @@ -0,0 +1,336 @@ +""" +Slack Connector OAuth Routes. + +Handles OAuth 2.0 authentication flow for Slack connector. +""" + +import logging +from datetime import UTC, datetime, timedelta +from uuid import UUID + +import httpx +from fastapi import APIRouter, Depends, HTTPException, Request +from fastapi.responses import RedirectResponse +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, + SearchSourceConnectorType, + User, + get_async_session, +) +from app.users import current_active_user +from app.utils.oauth_security import OAuthStateManager, TokenEncryption + +logger = logging.getLogger(__name__) + +router = APIRouter() + +# Slack OAuth endpoints +AUTHORIZATION_URL = "https://slack.com/oauth/v2/authorize" +TOKEN_URL = "https://slack.com/api/oauth.v2.access" + +# OAuth scopes for Slack (Bot Token) +SCOPES = [ + "channels:history", # Read messages in public channels + "channels:read", # View basic information about public channels + "groups:history", # Read messages in private channels + "groups:read", # View basic information about private channels + "im:history", # Read messages in direct messages + "mpim:history", # Read messages in group direct messages + "users:read", # Read user information +] + +# Initialize security utilities +_state_manager = None +_token_encryption = None + + +def get_state_manager() -> OAuthStateManager: + """Get or create OAuth state manager instance.""" + global _state_manager + if _state_manager is None: + if not config.SECRET_KEY: + raise ValueError("SECRET_KEY must be set for OAuth security") + _state_manager = OAuthStateManager(config.SECRET_KEY) + return _state_manager + + +def get_token_encryption() -> TokenEncryption: + """Get or create token encryption instance.""" + global _token_encryption + if _token_encryption is None: + if not config.SECRET_KEY: + raise ValueError("SECRET_KEY must be set for token encryption") + _token_encryption = TokenEncryption(config.SECRET_KEY) + return _token_encryption + + +@router.get("/auth/slack/connector/add") +async def connect_slack(space_id: int, user: User = Depends(current_active_user)): + """ + Initiate Slack OAuth flow. + + Args: + space_id: The search space ID + user: Current authenticated user + + Returns: + Authorization URL for redirect + """ + try: + if not space_id: + raise HTTPException(status_code=400, detail="space_id is required") + + if not config.SLACK_CLIENT_ID: + raise HTTPException(status_code=500, detail="Slack OAuth not configured.") + + if not config.SECRET_KEY: + raise HTTPException( + status_code=500, detail="SECRET_KEY not configured for OAuth security." + ) + + # Generate secure state parameter with HMAC signature + state_manager = get_state_manager() + state_encoded = state_manager.generate_secure_state(space_id, user.id) + + # Build authorization URL + from urllib.parse import urlencode + + auth_params = { + "client_id": config.SLACK_CLIENT_ID, + "scope": ",".join(SCOPES), + "redirect_uri": config.SLACK_REDIRECT_URI, + "state": state_encoded, + } + + auth_url = f"{AUTHORIZATION_URL}?{urlencode(auth_params)}" + + logger.info(f"Generated Slack OAuth URL for user {user.id}, space {space_id}") + return {"auth_url": auth_url} + + except Exception as e: + logger.error(f"Failed to initiate Slack OAuth: {e!s}", exc_info=True) + raise HTTPException( + status_code=500, detail=f"Failed to initiate Slack OAuth: {e!s}" + ) from e + + +@router.get("/auth/slack/connector/callback") +async def slack_callback( + request: Request, + code: str | None = None, + error: str | None = None, + state: str | None = None, + session: AsyncSession = Depends(get_async_session), +): + """ + Handle Slack OAuth callback. + + Args: + request: FastAPI request object + code: Authorization code from Slack (if user granted access) + error: Error code from Slack (if user denied access or error occurred) + state: State parameter containing user/space info + session: Database session + + Returns: + Redirect response to frontend + """ + try: + # Handle OAuth errors (e.g., user denied access) + if error: + logger.warning(f"Slack OAuth error: {error}") + # Try to decode state to get space_id for redirect, but don't fail if it's invalid + space_id = None + if state: + try: + state_manager = get_state_manager() + data = state_manager.validate_state(state) + space_id = data.get("space_id") + except Exception: + # If state is invalid, we'll redirect without space_id + logger.warning("Failed to validate state in error handler") + + # Redirect to frontend with error parameter + if space_id: + return RedirectResponse( + url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&error=slack_oauth_denied" + ) + else: + return RedirectResponse( + url=f"{config.NEXT_FRONTEND_URL}/dashboard?error=slack_oauth_denied" + ) + + # Validate required parameters for successful flow + if not code: + raise HTTPException(status_code=400, detail="Missing authorization code") + if not state: + raise HTTPException(status_code=400, detail="Missing state parameter") + + # Validate and decode state with signature verification + state_manager = get_state_manager() + try: + data = state_manager.validate_state(state) + except HTTPException: + raise + except Exception as e: + raise HTTPException( + status_code=400, detail=f"Invalid state parameter: {e!s}" + ) from e + + user_id = UUID(data["user_id"]) + space_id = data["space_id"] + + # Validate redirect URI (security: ensure it matches configured value) + if not config.SLACK_REDIRECT_URI: + raise HTTPException( + status_code=500, detail="SLACK_REDIRECT_URI not configured" + ) + + # Exchange authorization code for access token + token_data = { + "client_id": config.SLACK_CLIENT_ID, + "client_secret": config.SLACK_CLIENT_SECRET, + "code": code, + "redirect_uri": config.SLACK_REDIRECT_URI, + } + + async with httpx.AsyncClient() as client: + token_response = await client.post( + TOKEN_URL, + data=token_data, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + timeout=30.0, + ) + + if token_response.status_code != 200: + error_detail = token_response.text + try: + error_json = token_response.json() + error_detail = error_json.get("error", error_detail) + except Exception: + pass + raise HTTPException( + status_code=400, detail=f"Token exchange failed: {error_detail}" + ) + + token_json = token_response.json() + + # Slack OAuth v2 returns success status in the JSON + if not token_json.get("ok", False): + error_msg = token_json.get("error", "Unknown error") + raise HTTPException( + status_code=400, detail=f"Slack OAuth error: {error_msg}" + ) + + # Extract bot token from Slack response + # Slack OAuth v2 returns: { "ok": true, "access_token": "...", "bot": { "bot_user_id": "...", "bot_access_token": "xoxb-..." }, ... } + bot_token = None + if token_json.get("bot") and token_json["bot"].get("bot_access_token"): + bot_token = token_json["bot"]["bot_access_token"] + elif token_json.get("access_token"): + # Fallback to access_token if bot token not available + bot_token = token_json["access_token"] + else: + raise HTTPException( + status_code=400, detail="No bot token received from Slack" + ) + + # Encrypt sensitive tokens before storing + token_encryption = get_token_encryption() + + # Calculate expiration time (UTC, tz-aware) + # Slack tokens don't expire by default, but we'll store expiration info if provided + expires_at = None + if token_json.get("expires_in"): + now_utc = datetime.now(UTC) + expires_at = now_utc + timedelta(seconds=int(token_json["expires_in"])) + + # Store the encrypted bot token in connector config + connector_config = { + "bot_token": token_encryption.encrypt_token(bot_token), + "bot_user_id": token_json.get("bot", {}).get("bot_user_id"), + "team_id": token_json.get("team", {}).get("id"), + "team_name": token_json.get("team", {}).get("name"), + "token_type": token_json.get("token_type", "Bearer"), + "expires_in": token_json.get("expires_in"), + "expires_at": expires_at.isoformat() if expires_at else None, + "scope": token_json.get("scope"), + # Mark that tokens are encrypted for backward compatibility + "_token_encrypted": True, + } + + # Check if connector already exists for this search space and user + existing_connector_result = await session.execute( + select(SearchSourceConnector).filter( + SearchSourceConnector.search_space_id == space_id, + SearchSourceConnector.user_id == user_id, + SearchSourceConnector.connector_type + == SearchSourceConnectorType.SLACK_CONNECTOR, + ) + ) + existing_connector = existing_connector_result.scalars().first() + + if existing_connector: + # Update existing connector + existing_connector.config = connector_config + existing_connector.name = "Slack Connector" + existing_connector.is_indexable = True + logger.info( + f"Updated existing Slack connector for user {user_id} in space {space_id}" + ) + else: + # Create new connector + new_connector = SearchSourceConnector( + name="Slack Connector", + connector_type=SearchSourceConnectorType.SLACK_CONNECTOR, + is_indexable=True, + config=connector_config, + search_space_id=space_id, + user_id=user_id, + ) + session.add(new_connector) + logger.info( + f"Created new Slack connector for user {user_id} in space {space_id}" + ) + + try: + await session.commit() + logger.info(f"Successfully saved Slack connector for user {user_id}") + + # Redirect to the frontend with success params + return RedirectResponse( + url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&success=true&connector=slack-connector" + ) + + except ValidationError as e: + await session.rollback() + raise HTTPException( + status_code=422, detail=f"Validation error: {e!s}" + ) from e + except IntegrityError as e: + await session.rollback() + raise HTTPException( + status_code=409, + detail=f"Integrity error: A connector with this type already exists. {e!s}", + ) from e + except Exception as e: + logger.error(f"Failed to create search source connector: {e!s}") + await session.rollback() + raise HTTPException( + status_code=500, + detail=f"Failed to create search source connector: {e!s}", + ) from e + + except HTTPException: + raise + except Exception as e: + logger.error(f"Failed to complete Slack OAuth: {e!s}", exc_info=True) + raise HTTPException( + status_code=500, detail=f"Failed to complete Slack OAuth: {e!s}" + ) from e diff --git a/surfsense_backend/app/schemas/search_source_connector.py b/surfsense_backend/app/schemas/search_source_connector.py index b0d8ebc3a..dbe4dce1f 100644 --- a/surfsense_backend/app/schemas/search_source_connector.py +++ b/surfsense_backend/app/schemas/search_source_connector.py @@ -31,7 +31,7 @@ class SearchSourceConnectorBase(BaseModel): @model_validator(mode="after") def validate_periodic_indexing(self): """Validate that periodic indexing configuration is consistent. - + Supported frequencies: Any positive integer (in minutes). Common values: 5, 15, 60 (1 hour), 360 (6 hours), 720 (12 hours), 1440 (daily), etc. The schedule checker will handle any frequency >= 1 minute. diff --git a/surfsense_backend/app/tasks/connector_indexers/slack_indexer.py b/surfsense_backend/app/tasks/connector_indexers/slack_indexer.py index 5119aba2e..4c4191a4e 100644 --- a/surfsense_backend/app/tasks/connector_indexers/slack_indexer.py +++ b/surfsense_backend/app/tasks/connector_indexers/slack_indexer.py @@ -17,6 +17,7 @@ from app.utils.document_converters import ( generate_content_hash, generate_unique_identifier_hash, ) +from app.utils.oauth_security import TokenEncryption from .base import ( build_document_metadata_markdown, @@ -93,7 +94,10 @@ async def index_slack_messages( ) # Get the Slack token from the connector config - slack_token = connector.config.get("SLACK_BOT_TOKEN") + # Support both new OAuth format (bot_token) and old API format (SLACK_BOT_TOKEN) + config_data = connector.config.copy() + slack_token = config_data.get("bot_token") or config_data.get("SLACK_BOT_TOKEN") + if not slack_token: await task_logger.log_task_failure( log_entry, @@ -103,6 +107,22 @@ async def index_slack_messages( ) return 0, "Slack token not found in connector config" + # Decrypt token if it's encrypted (OAuth format) + token_encrypted = config_data.get("_token_encrypted", False) + if token_encrypted and config.SECRET_KEY: + try: + token_encryption = TokenEncryption(config.SECRET_KEY) + slack_token = token_encryption.decrypt_token(slack_token) + logger.info(f"Decrypted Slack bot token for connector {connector_id}") + except Exception as e: + await task_logger.log_task_failure( + log_entry, + f"Failed to decrypt Slack token for connector {connector_id}: {e!s}", + "Token decryption failed", + {"error_type": "TokenDecryptionError"}, + ) + return 0, f"Failed to decrypt Slack token: {e!s}" + # Initialize Slack client await task_logger.log_task_progress( log_entry, @@ -112,6 +132,12 @@ async def index_slack_messages( slack_client = SlackHistory(token=slack_token) + # Handle 'undefined' string from frontend (treat as None) + if start_date == "undefined" or start_date == "": + start_date = None + if end_date == "undefined" or end_date == "": + end_date = None + # Calculate date range await task_logger.log_task_progress( log_entry, diff --git a/surfsense_backend/app/utils/validators.py b/surfsense_backend/app/utils/validators.py index d6622bafd..8db6ed4a3 100644 --- a/surfsense_backend/app/utils/validators.py +++ b/surfsense_backend/app/utils/validators.py @@ -513,7 +513,22 @@ def validate_connector_config( ], "validators": {}, }, - "SLACK_CONNECTOR": {"required": ["SLACK_BOT_TOKEN"], "validators": {}}, + # "SLACK_CONNECTOR": { + # "required": [], # OAuth uses bot_token (encrypted), legacy uses SLACK_BOT_TOKEN + # "optional": [ + # "bot_token", + # "SLACK_BOT_TOKEN", + # "bot_user_id", + # "team_id", + # "team_name", + # "token_type", + # "expires_in", + # "expires_at", + # "scope", + # "_token_encrypted", + # ], + # "validators": {}, + # }, "GITHUB_CONNECTOR": { "required": ["GITHUB_PAT", "repo_full_names"], "validators": { diff --git a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/slack-connect-form.tsx b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/slack-connect-form.tsx deleted file mode 100644 index 3952144e6..000000000 --- a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/slack-connect-form.tsx +++ /dev/null @@ -1,429 +0,0 @@ -"use client"; - -import { zodResolver } from "@hookform/resolvers/zod"; -import { Info } from "lucide-react"; -import type { FC } from "react"; -import { useRef, useState } from "react"; -import { useForm } from "react-hook-form"; -import * as z from "zod"; -import { - Accordion, - AccordionContent, - AccordionItem, - AccordionTrigger, -} from "@/components/ui/accordion"; -import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; -import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { Switch } from "@/components/ui/switch"; -import { EnumConnectorName } from "@/contracts/enums/connector"; -import { DateRangeSelector } from "../../components/date-range-selector"; -import { getConnectorBenefits } from "../connector-benefits"; -import type { ConnectFormProps } from "../index"; - -const slackConnectorFormSchema = z.object({ - name: z.string().min(3, { - message: "Connector name must be at least 3 characters.", - }), - bot_token: z.string().min(10, { - message: "Slack Bot Token is required and must be valid.", - }), -}); - -type SlackConnectorFormValues = z.infer; - -export const SlackConnectForm: FC = ({ onSubmit, isSubmitting }) => { - const isSubmittingRef = useRef(false); - const [startDate, setStartDate] = useState(undefined); - const [endDate, setEndDate] = useState(undefined); - const [periodicEnabled, setPeriodicEnabled] = useState(false); - const [frequencyMinutes, setFrequencyMinutes] = useState("1440"); - const form = useForm({ - resolver: zodResolver(slackConnectorFormSchema), - defaultValues: { - name: "Slack Connector", - bot_token: "", - }, - }); - - const handleSubmit = async (values: SlackConnectorFormValues) => { - // Prevent multiple submissions - if (isSubmittingRef.current || isSubmitting) { - return; - } - - isSubmittingRef.current = true; - try { - await onSubmit({ - name: values.name, - connector_type: EnumConnectorName.SLACK_CONNECTOR, - config: { - SLACK_BOT_TOKEN: values.bot_token, - }, - is_indexable: true, - last_indexed_at: null, - periodic_indexing_enabled: periodicEnabled, - indexing_frequency_minutes: periodicEnabled ? parseInt(frequencyMinutes, 10) : null, - next_scheduled_at: null, - startDate, - endDate, - periodicEnabled, - frequencyMinutes, - }); - } finally { - isSubmittingRef.current = false; - } - }; - - return ( -
- - -
- Bot User OAuth Token Required - - You'll need a Slack Bot User OAuth Token to use this connector. You can create a Slack - app and get the token from{" "} - - Slack API Dashboard - - -
-
- -
-
- - ( - - Connector Name - - - - - A friendly name to identify this connector. - - - - )} - /> - - ( - - Slack Bot User OAuth Token - - - - - Your Bot User OAuth Token will be encrypted and stored securely. It typically - starts with "xoxb-". - - - - )} - /> - - {/* Indexing Configuration */} -
-

Indexing Configuration

- - {/* Date Range Selector */} - - - {/* Periodic Sync Config */} -
-
-
-

Enable Periodic Sync

-

- Automatically re-index at regular intervals -

-
- -
- - {periodicEnabled && ( -
-
- - -
-
- )} -
-
- - -
- - {/* What you get section */} - {getConnectorBenefits(EnumConnectorName.SLACK_CONNECTOR) && ( -
-

What you get with Slack integration:

-
    - {getConnectorBenefits(EnumConnectorName.SLACK_CONNECTOR)?.map((benefit) => ( -
  • {benefit}
  • - ))} -
-
- )} - - {/* Documentation Section */} - - - - Documentation - - -
-

How it works

-

- The Slack connector uses the Slack Web API to fetch messages from all accessible - channels that the bot token has access to within a workspace. -

-
    -
  • - For follow up indexing runs, the connector retrieves messages that have been - updated since the last indexing attempt. -
  • -
  • - Indexing is configured to run periodically, so updates should appear in your - search results within minutes. -
  • -
-
- -
-
-

Authorization

- - - - Bot User OAuth Token Required - - - You need to create a Slack app and install it to your workspace to get a Bot - User OAuth Token. The bot needs read access to channels and messages. - - - -
-
-

- Step 1: Create a Slack App -

-
    -
  1. - Go to{" "} - - https://api.slack.com/apps - -
  2. -
  3. - Click Create New App and choose "From scratch" -
  4. -
  5. Enter an app name and select your workspace
  6. -
  7. - Click Create App -
  8. -
-
- -
-

- Step 2: Configure Bot Scopes -

-
    -
  1. - Navigate to OAuth & Permissions in the sidebar -
  2. -
  3. - Under Bot Token Scopes, add the following scopes: -
      -
    • - channels:read - - View basic information about public channels -
    • -
    • - channels:history - - View messages in public channels -
    • -
    • - groups:read - View - basic information about private channels -
    • -
    • - groups:history - - View messages in private channels -
    • -
    • - im:read - View - basic information about direct messages -
    • -
    • - im:history - View - messages in direct messages -
    • -
    -
  4. -
-
- -
-

- Step 3: Install App to Workspace -

-
    -
  1. - Go to Install App in the sidebar -
  2. -
  3. - Click Install to Workspace -
  4. -
  5. - Review the permissions and click Allow -
  6. -
  7. - Copy the Bot User OAuth Token from the "OAuth & - Permissions" page (starts with "xoxb-") -
  8. -
-
-
-
-
- -
-
-

Indexing

-
    -
  1. - Navigate to the Connector Dashboard and select the Slack{" "} - Connector. -
  2. -
  3. - Place the Bot User OAuth Token in the form field. -
  4. -
  5. - Click Connect to establish the connection. -
  6. -
  7. Once connected, your Slack messages will be indexed automatically.
  8. -
- - - - What Gets Indexed - -

The Slack connector indexes the following data:

-
    -
  • Messages from all accessible channels (public and private)
  • -
  • Direct messages (if bot has access)
  • -
  • Message timestamps and metadata
  • -
  • Thread replies and conversations
  • -
-
-
-
-
-
-
-
-
- ); -}; diff --git a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/index.tsx b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/index.tsx index 55169fadd..807d4cb7a 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/index.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/index.tsx @@ -11,7 +11,6 @@ import { JiraConnectForm } from "./components/jira-connect-form"; import { LinkupApiConnectForm } from "./components/linkup-api-connect-form"; import { LumaConnectForm } from "./components/luma-connect-form"; import { SearxngConnectForm } from "./components/searxng-connect-form"; -import { SlackConnectForm } from "./components/slack-connect-form"; import { TavilyApiConnectForm } from "./components/tavily-api-connect-form"; export interface ConnectFormProps { @@ -51,8 +50,6 @@ export function getConnectFormComponent(connectorType: string): ConnectFormCompo return BaiduSearchApiConnectForm; case "ELASTICSEARCH_CONNECTOR": return ElasticsearchConnectForm; - case "SLACK_CONNECTOR": - return SlackConnectForm; case "DISCORD_CONNECTOR": return DiscordConnectForm; case "CONFLUENCE_CONNECTOR": diff --git a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/slack-config.tsx b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/slack-config.tsx index 73ae6a4f3..58293c4de 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/slack-config.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/slack-config.tsx @@ -1,84 +1,27 @@ "use client"; -import { KeyRound } from "lucide-react"; +import { Info } from "lucide-react"; import type { FC } from "react"; -import { useEffect, useState } from "react"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; import type { ConnectorConfigProps } from "../index"; export interface SlackConfigProps extends ConnectorConfigProps { onNameChange?: (name: string) => void; } -export const SlackConfig: FC = ({ connector, onConfigChange, onNameChange }) => { - const [botToken, setBotToken] = useState( - (connector.config?.SLACK_BOT_TOKEN as string) || "" - ); - const [name, setName] = useState(connector.name || ""); - - // Update bot token and name when connector changes - useEffect(() => { - const token = (connector.config?.SLACK_BOT_TOKEN as string) || ""; - setBotToken(token); - setName(connector.name || ""); - }, [connector.config, connector.name]); - - const handleBotTokenChange = (value: string) => { - setBotToken(value); - if (onConfigChange) { - onConfigChange({ - ...connector.config, - SLACK_BOT_TOKEN: value, - }); - } - }; - - const handleNameChange = (value: string) => { - setName(value); - if (onNameChange) { - onNameChange(value); - } - }; - +export const SlackConfig: FC = () => { return (
- {/* Connector Name */} -
-
- - handleNameChange(e.target.value)} - placeholder="My Slack Connector" - className="border-slate-400/20 focus-visible:border-slate-400/40" - /> -

- A friendly name to identify this connector. -

+
+
+
-
- - {/* Configuration */} -
-
-

Configuration

-
- -
- - handleBotTokenChange(e.target.value)} - placeholder="Begins with xoxb-..." - className="border-slate-400/20 focus-visible:border-slate-400/40" - /> -

- Update your Bot User OAuth Token if needed. +

+

Add Bot to Channels

+

+ Before indexing, add the SurfSense bot to each channel you want to index. The bot can + only access messages from channels it's been added to. Type{" "} + /invite @SurfSense in + any channel to add it.

diff --git a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-connect-view.tsx b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-connect-view.tsx index a02ae5088..5437426c8 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-connect-view.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-connect-view.tsx @@ -52,7 +52,6 @@ export const ConnectorConnectView: FC = ({ LINKUP_API: "linkup-api-connect-form", BAIDU_SEARCH_API: "baidu-search-api-connect-form", ELASTICSEARCH_CONNECTOR: "elasticsearch-connect-form", - SLACK_CONNECTOR: "slack-connect-form", DISCORD_CONNECTOR: "discord-connect-form", CONFLUENCE_CONNECTOR: "confluence-connect-form", BOOKSTACK_CONNECTOR: "bookstack-connect-form", diff --git a/surfsense_web/components/assistant-ui/connector-popup/constants/connector-constants.ts b/surfsense_web/components/assistant-ui/connector-popup/constants/connector-constants.ts index 0ea263430..111b7485d 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/constants/connector-constants.ts +++ b/surfsense_web/components/assistant-ui/connector-popup/constants/connector-constants.ts @@ -44,6 +44,13 @@ export const OAUTH_CONNECTORS = [ connectorType: EnumConnectorName.LINEAR_CONNECTOR, authEndpoint: "/api/v1/auth/linear/connector/add/", }, + { + id: "slack-connector", + title: "Slack", + description: "Search Slack messages", + connectorType: EnumConnectorName.SLACK_CONNECTOR, + authEndpoint: "/api/v1/auth/slack/connector/add/", + }, ] as const; // Content Sources (tools that extract and import content from external sources) @@ -64,12 +71,6 @@ export const CRAWLERS = [ // Non-OAuth Connectors (redirect to old connector config pages) export const OTHER_CONNECTORS = [ - { - id: "slack-connector", - title: "Slack", - description: "Search Slack messages", - connectorType: EnumConnectorName.SLACK_CONNECTOR, - }, { id: "discord-connector", title: "Discord", From 81e4a4ada06343e27d7cdcf3e3b757b4c9b11d69 Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Sun, 4 Jan 2026 02:38:19 +0530 Subject: [PATCH 15/75] feat: database driven refresh tokens for slack oauth connector --- .../app/connectors/slack_history.py | 217 ++++++++++++++++-- .../app/routes/slack_add_connector_route.py | 146 +++++++++++- .../app/schemas/slack_auth_credentials.py | 76 ++++++ .../tasks/connector_indexers/slack_indexer.py | 45 +--- 4 files changed, 426 insertions(+), 58 deletions(-) create mode 100644 surfsense_backend/app/schemas/slack_auth_credentials.py diff --git a/surfsense_backend/app/connectors/slack_history.py b/surfsense_backend/app/connectors/slack_history.py index 36160c30b..6a016394e 100644 --- a/surfsense_backend/app/connectors/slack_history.py +++ b/surfsense_backend/app/connectors/slack_history.py @@ -12,6 +12,14 @@ from typing import Any from slack_sdk import WebClient from slack_sdk.errors import SlackApiError +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.future import select + +from app.config import config +from app.db import SearchSourceConnector +from app.routes.slack_add_connector_route import refresh_slack_token +from app.schemas.slack_auth_credentials import SlackAuthCredentialsBase +from app.utils.oauth_security import TokenEncryption logger = logging.getLogger(__name__) # Added logger @@ -19,25 +27,195 @@ logger = logging.getLogger(__name__) # Added logger class SlackHistory: """Class for retrieving conversation history from Slack channels.""" - def __init__(self, token: str | None = None): + def __init__( + self, + token: str | None = None, + session: AsyncSession | None = None, + connector_id: int | None = None, + credentials: SlackAuthCredentialsBase | None = None, + ): """ Initialize the SlackHistory class. Args: - token: Slack API token (optional, can be set later with set_token) + token: Slack API token (optional, for backward compatibility) + session: Database session for token refresh (optional) + connector_id: Connector ID for token refresh (optional) + credentials: Slack OAuth credentials (optional, will be loaded from DB if not provided) """ - self.client = WebClient(token=token) if token else None + self._session = session + self._connector_id = connector_id + self._credentials = credentials + # For backward compatibility, if token is provided directly, use it + if token: + self.client = WebClient(token=token) + else: + self.client = None + + async def _get_valid_token(self) -> str: + """ + Get valid Slack bot token, refreshing if needed. + + Returns: + Valid bot token + + Raises: + ValueError: If credentials are missing or invalid + Exception: If token refresh fails + """ + # If we have a direct token (backward compatibility), use it + # Check if client was initialized with a token directly (not via credentials) + if self.client and self._session is None and self._connector_id is None: + # This means it was initialized with a direct token, extract it + # WebClient stores token internally, we need to get it from the client + # For backward compatibility, we'll use the client directly + # But we can't easily extract the token, so we'll just use the client + # In this case, we'll skip refresh logic + if self._credentials is None: + # This is the old pattern - just use the client as-is + # We can't extract token easily, so we'll raise an error + # asking to use the new pattern + raise ValueError( + "Cannot refresh token: Please use session and connector_id for auto-refresh support" + ) + + # Load credentials from DB if not provided + if self._credentials is None: + if not self._session or not self._connector_id: + raise ValueError( + "Cannot load credentials: session and connector_id required" + ) + + result = await self._session.execute( + select(SearchSourceConnector).filter( + SearchSourceConnector.id == self._connector_id + ) + ) + connector = result.scalars().first() + + if not connector: + raise ValueError(f"Connector {self._connector_id} not found") + + config_data = connector.config.copy() + + # Decrypt credentials if they are encrypted + token_encrypted = config_data.get("_token_encrypted", False) + if token_encrypted and config.SECRET_KEY: + try: + token_encryption = TokenEncryption(config.SECRET_KEY) + + # Decrypt sensitive fields + if config_data.get("bot_token"): + config_data["bot_token"] = token_encryption.decrypt_token( + config_data["bot_token"] + ) + if config_data.get("refresh_token"): + config_data["refresh_token"] = token_encryption.decrypt_token( + config_data["refresh_token"] + ) + + logger.info( + f"Decrypted Slack credentials for connector {self._connector_id}" + ) + except Exception as e: + logger.error( + f"Failed to decrypt Slack credentials for connector {self._connector_id}: {e!s}" + ) + raise ValueError( + f"Failed to decrypt Slack credentials: {e!s}" + ) from e + + try: + self._credentials = SlackAuthCredentialsBase.from_dict(config_data) + except Exception as e: + raise ValueError(f"Invalid Slack credentials: {e!s}") from e + + # Check if token is expired and refreshable + if self._credentials.is_expired and self._credentials.is_refreshable: + try: + logger.info( + f"Slack token expired for connector {self._connector_id}, refreshing..." + ) + + # Get connector for refresh + result = await self._session.execute( + select(SearchSourceConnector).filter( + SearchSourceConnector.id == self._connector_id + ) + ) + connector = result.scalars().first() + + if not connector: + raise RuntimeError( + f"Connector {self._connector_id} not found; cannot refresh token." + ) + + # Refresh token + connector = await refresh_slack_token(self._session, connector) + + # Reload credentials after refresh + config_data = connector.config.copy() + token_encrypted = config_data.get("_token_encrypted", False) + if token_encrypted and config.SECRET_KEY: + token_encryption = TokenEncryption(config.SECRET_KEY) + if config_data.get("bot_token"): + config_data["bot_token"] = token_encryption.decrypt_token( + config_data["bot_token"] + ) + if config_data.get("refresh_token"): + config_data["refresh_token"] = token_encryption.decrypt_token( + config_data["refresh_token"] + ) + + self._credentials = SlackAuthCredentialsBase.from_dict(config_data) + + # Invalidate cached client so it's recreated with new token + self.client = None + + logger.info( + f"Successfully refreshed Slack token for connector {self._connector_id}" + ) + except Exception as e: + logger.error( + f"Failed to refresh Slack token for connector {self._connector_id}: {e!s}" + ) + raise Exception( + f"Failed to refresh Slack OAuth credentials: {e!s}" + ) from e + + return self._credentials.bot_token + + async def _ensure_client(self) -> WebClient: + """ + Ensure Slack client is initialized with valid token. + + Returns: + WebClient instance + """ + # If client was initialized with direct token (backward compatibility), use it + if self.client and (self._session is None or self._connector_id is None): + return self.client + + # Otherwise, initialize with token from credentials (with auto-refresh) + if self.client is None: + token = await self._get_valid_token() + # Skip if it's the placeholder for direct token initialization + if token != "direct_token_initialized": + self.client = WebClient(token=token) + return self.client def set_token(self, token: str) -> None: """ - Set the Slack API token. + Set the Slack API token (for backward compatibility). Args: token: Slack API token """ self.client = WebClient(token=token) - def get_all_channels(self, include_private: bool = True) -> list[dict[str, Any]]: + async def get_all_channels( + self, include_private: bool = True + ) -> list[dict[str, Any]]: """ Fetch all channels that the bot has access to, with rate limit handling. @@ -52,8 +230,7 @@ class SlackHistory: SlackApiError: If there's an unrecoverable error calling the Slack API RuntimeError: For unexpected errors during channel fetching. """ - if not self.client: - raise ValueError("Slack client not initialized. Call set_token() first.") + client = await self._ensure_client() channels_list = [] # Changed from dict to list types = "public_channel" @@ -72,7 +249,7 @@ class SlackHistory: time.sleep(3) current_limit = 1000 # Max limit - api_result = self.client.conversations_list( + api_result = client.conversations_list( types=types, cursor=next_cursor, limit=current_limit ) @@ -129,7 +306,7 @@ class SlackHistory: return channels_list - def get_conversation_history( + async def get_conversation_history( self, channel_id: str, limit: int = 1000, @@ -152,8 +329,7 @@ class SlackHistory: ValueError: If no Slack client has been initialized SlackApiError: If there's an error calling the Slack API """ - if not self.client: - raise ValueError("Slack client not initialized. Call set_token() first.") + client = await self._ensure_client() messages = [] next_cursor = None @@ -177,7 +353,7 @@ class SlackHistory: current_api_call_successful = False result = None # Ensure result is defined try: - result = self.client.conversations_history(**kwargs) + result = client.conversations_history(**kwargs) current_api_call_successful = True except SlackApiError as e_history: if ( @@ -252,7 +428,7 @@ class SlackHistory: except ValueError: return None - def get_history_by_date_range( + async def get_history_by_date_range( self, channel_id: str, start_date: str, end_date: str, limit: int = 1000 ) -> tuple[list[dict[str, Any]], str | None]: """ @@ -282,7 +458,7 @@ class SlackHistory: latest += 86400 # seconds in a day try: - messages = self.get_conversation_history( + messages = await self.get_conversation_history( channel_id=channel_id, limit=limit, oldest=oldest, latest=latest ) return messages, None @@ -291,7 +467,7 @@ class SlackHistory: except ValueError as e: return [], str(e) - def get_user_info(self, user_id: str) -> dict[str, Any]: + async def get_user_info(self, user_id: str) -> dict[str, Any]: """ Get information about a user. @@ -305,8 +481,7 @@ class SlackHistory: ValueError: If no Slack client has been initialized SlackApiError: If there's an error calling the Slack API """ - if not self.client: - raise ValueError("Slack client not initialized. Call set_token() first.") + client = await self._ensure_client() while True: try: @@ -314,7 +489,7 @@ class SlackHistory: # For now, we are only adding Retry-After as per plan. # time.sleep(0.6) # Optional: ~100 req/min if ever needed. - result = self.client.users_info(user=user_id) + result = client.users_info(user=user_id) return result["user"] # Success, return and exit loop implicitly except SlackApiError as e_user_info: @@ -343,7 +518,7 @@ class SlackHistory: ) raise general_error from general_error # Re-raise unexpected errors - def format_message( + async def format_message( self, msg: dict[str, Any], include_user_info: bool = False ) -> dict[str, Any]: """ @@ -369,9 +544,9 @@ class SlackHistory: "is_thread": "thread_ts" in msg, } - if include_user_info and "user" in msg and self.client: + if include_user_info and "user" in msg: try: - user_info = self.get_user_info(msg["user"]) + user_info = await self.get_user_info(msg["user"]) formatted["user_name"] = user_info.get("real_name", "Unknown") formatted["user_email"] = user_info.get("profile", {}).get("email", "") except Exception: diff --git a/surfsense_backend/app/routes/slack_add_connector_route.py b/surfsense_backend/app/routes/slack_add_connector_route.py index 1bbb4f5f1..71a362119 100644 --- a/surfsense_backend/app/routes/slack_add_connector_route.py +++ b/surfsense_backend/app/routes/slack_add_connector_route.py @@ -23,6 +23,7 @@ from app.db import ( User, get_async_session, ) +from app.schemas.slack_auth_credentials import SlackAuthCredentialsBase from app.users import current_active_user from app.utils.oauth_security import OAuthStateManager, TokenEncryption @@ -229,7 +230,7 @@ async def slack_callback( ) # Extract bot token from Slack response - # Slack OAuth v2 returns: { "ok": true, "access_token": "...", "bot": { "bot_user_id": "...", "bot_access_token": "xoxb-..." }, ... } + # Slack OAuth v2 returns: { "ok": true, "access_token": "...", "bot": { "bot_user_id": "...", "bot_access_token": "xoxb-..." }, "refresh_token": "...", ... } bot_token = None if token_json.get("bot") and token_json["bot"].get("bot_access_token"): bot_token = token_json["bot"]["bot_access_token"] @@ -241,6 +242,9 @@ async def slack_callback( status_code=400, detail="No bot token received from Slack" ) + # Extract refresh token if available (for token rotation) + refresh_token = token_json.get("refresh_token") + # Encrypt sensitive tokens before storing token_encryption = get_token_encryption() @@ -251,9 +255,12 @@ async def slack_callback( now_utc = datetime.now(UTC) expires_at = now_utc + timedelta(seconds=int(token_json["expires_in"])) - # Store the encrypted bot token in connector config + # Store the encrypted bot token and refresh token in connector config connector_config = { "bot_token": token_encryption.encrypt_token(bot_token), + "refresh_token": token_encryption.encrypt_token(refresh_token) + if refresh_token + else None, "bot_user_id": token_json.get("bot", {}).get("bot_user_id"), "team_id": token_json.get("team", {}).get("id"), "team_name": token_json.get("team", {}).get("name"), @@ -334,3 +341,138 @@ async def slack_callback( raise HTTPException( status_code=500, detail=f"Failed to complete Slack OAuth: {e!s}" ) from e + + +async def refresh_slack_token( + session: AsyncSession, connector: SearchSourceConnector +) -> SearchSourceConnector: + """ + Refresh the Slack bot token for a connector. + + Args: + session: Database session + connector: Slack connector to refresh + + Returns: + Updated connector object + """ + try: + logger.info(f"Refreshing Slack token for connector {connector.id}") + + credentials = SlackAuthCredentialsBase.from_dict(connector.config) + + # Decrypt tokens if they are encrypted + token_encryption = get_token_encryption() + is_encrypted = connector.config.get("_token_encrypted", False) + + refresh_token = credentials.refresh_token + if is_encrypted and refresh_token: + try: + refresh_token = token_encryption.decrypt_token(refresh_token) + except Exception as e: + logger.error(f"Failed to decrypt refresh token: {e!s}") + raise HTTPException( + status_code=500, detail="Failed to decrypt stored refresh token" + ) from e + + if not refresh_token: + raise HTTPException( + status_code=400, + detail="No refresh token available. Please re-authenticate.", + ) + + # Slack uses oauth.v2.access for token refresh with grant_type=refresh_token + refresh_data = { + "client_id": config.SLACK_CLIENT_ID, + "client_secret": config.SLACK_CLIENT_SECRET, + "grant_type": "refresh_token", + "refresh_token": refresh_token, + } + + async with httpx.AsyncClient() as client: + token_response = await client.post( + TOKEN_URL, + data=refresh_data, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + timeout=30.0, + ) + + if token_response.status_code != 200: + error_detail = token_response.text + try: + error_json = token_response.json() + error_detail = error_json.get("error", error_detail) + except Exception: + pass + raise HTTPException( + status_code=400, detail=f"Token refresh failed: {error_detail}" + ) + + token_json = token_response.json() + + # Slack OAuth v2 returns success status in the JSON + if not token_json.get("ok", False): + error_msg = token_json.get("error", "Unknown error") + raise HTTPException( + status_code=400, detail=f"Slack OAuth refresh error: {error_msg}" + ) + + # Extract bot token from refresh response + bot_token = None + if token_json.get("bot") and token_json["bot"].get("bot_access_token"): + bot_token = token_json["bot"]["bot_access_token"] + elif token_json.get("access_token"): + bot_token = token_json["access_token"] + else: + raise HTTPException( + status_code=400, detail="No bot token received from Slack refresh" + ) + + # Get new refresh token if provided (Slack may rotate refresh tokens) + new_refresh_token = token_json.get("refresh_token") + + # Calculate expiration time (UTC, tz-aware) + expires_at = None + expires_in = token_json.get("expires_in") + if expires_in: + now_utc = datetime.now(UTC) + expires_at = now_utc + timedelta(seconds=int(expires_in)) + + # Update credentials object with encrypted tokens + credentials.bot_token = token_encryption.encrypt_token(bot_token) + if new_refresh_token: + credentials.refresh_token = token_encryption.encrypt_token( + new_refresh_token + ) + credentials.expires_in = expires_in + credentials.expires_at = expires_at + credentials.scope = token_json.get("scope") + + # Preserve team info + if not credentials.team_id: + credentials.team_id = connector.config.get("team_id") + if not credentials.team_name: + credentials.team_name = connector.config.get("team_name") + if not credentials.bot_user_id: + credentials.bot_user_id = connector.config.get("bot_user_id") + + # Update connector config with encrypted tokens + credentials_dict = credentials.to_dict() + credentials_dict["_token_encrypted"] = True + connector.config = credentials_dict + await session.commit() + await session.refresh(connector) + + logger.info(f"Successfully refreshed Slack token for connector {connector.id}") + + return connector + except HTTPException: + raise + except Exception as e: + logger.error( + f"Failed to refresh Slack token for connector {connector.id}: {e!s}", + exc_info=True, + ) + raise HTTPException( + status_code=500, detail=f"Failed to refresh Slack token: {e!s}" + ) from e diff --git a/surfsense_backend/app/schemas/slack_auth_credentials.py b/surfsense_backend/app/schemas/slack_auth_credentials.py new file mode 100644 index 000000000..ad6a713ef --- /dev/null +++ b/surfsense_backend/app/schemas/slack_auth_credentials.py @@ -0,0 +1,76 @@ +from datetime import UTC, datetime + +from pydantic import BaseModel, field_validator + + +class SlackAuthCredentialsBase(BaseModel): + bot_token: str + refresh_token: str | None = None + token_type: str = "Bearer" + expires_in: int | None = None + expires_at: datetime | None = None + scope: str | None = None + bot_user_id: str | None = None + team_id: str | None = None + team_name: str | None = None + + @property + def is_expired(self) -> bool: + """Check if the credentials have expired.""" + if self.expires_at is None: + return False # Long-lived token, treat as not expired + return self.expires_at <= datetime.now(UTC) + + @property + def is_refreshable(self) -> bool: + """Check if the credentials can be refreshed.""" + return self.refresh_token is not None + + def to_dict(self) -> dict: + """Convert credentials to dictionary for storage.""" + return { + "bot_token": self.bot_token, + "refresh_token": self.refresh_token, + "token_type": self.token_type, + "expires_in": self.expires_in, + "expires_at": self.expires_at.isoformat() if self.expires_at else None, + "scope": self.scope, + "bot_user_id": self.bot_user_id, + "team_id": self.team_id, + "team_name": self.team_name, + } + + @classmethod + def from_dict(cls, data: dict) -> "SlackAuthCredentialsBase": + """Create credentials from dictionary.""" + expires_at = None + if data.get("expires_at"): + expires_at = datetime.fromisoformat(data["expires_at"]) + + return cls( + bot_token=data.get("bot_token", ""), + refresh_token=data.get("refresh_token"), + token_type=data.get("token_type", "Bearer"), + expires_in=data.get("expires_in"), + expires_at=expires_at, + scope=data.get("scope"), + bot_user_id=data.get("bot_user_id"), + team_id=data.get("team_id"), + team_name=data.get("team_name"), + ) + + @field_validator("expires_at", mode="before") + @classmethod + def ensure_aware_utc(cls, v): + # Strings like "2025-08-26T14:46:57.367184" + if isinstance(v, str): + # add +00:00 if missing tz info + if v.endswith("Z"): + return datetime.fromisoformat(v.replace("Z", "+00:00")) + dt = datetime.fromisoformat(v) + return dt if dt.tzinfo else dt.replace(tzinfo=UTC) + # datetime objects + if isinstance(v, datetime): + return v if v.tzinfo else v.replace(tzinfo=UTC) + return v + diff --git a/surfsense_backend/app/tasks/connector_indexers/slack_indexer.py b/surfsense_backend/app/tasks/connector_indexers/slack_indexer.py index 4c4191a4e..c7a815634 100644 --- a/surfsense_backend/app/tasks/connector_indexers/slack_indexer.py +++ b/surfsense_backend/app/tasks/connector_indexers/slack_indexer.py @@ -17,7 +17,6 @@ from app.utils.document_converters import ( generate_content_hash, generate_unique_identifier_hash, ) -from app.utils.oauth_security import TokenEncryption from .base import ( build_document_metadata_markdown, @@ -93,44 +92,20 @@ async def index_slack_messages( f"Connector with ID {connector_id} not found or is not a Slack connector", ) - # Get the Slack token from the connector config - # Support both new OAuth format (bot_token) and old API format (SLACK_BOT_TOKEN) - config_data = connector.config.copy() - slack_token = config_data.get("bot_token") or config_data.get("SLACK_BOT_TOKEN") + # Note: Token handling is now done automatically by SlackHistory + # with auto-refresh support. We just need to pass session and connector_id. - if not slack_token: - await task_logger.log_task_failure( - log_entry, - f"Slack token not found in connector config for connector {connector_id}", - "Missing Slack token", - {"error_type": "MissingToken"}, - ) - return 0, "Slack token not found in connector config" - - # Decrypt token if it's encrypted (OAuth format) - token_encrypted = config_data.get("_token_encrypted", False) - if token_encrypted and config.SECRET_KEY: - try: - token_encryption = TokenEncryption(config.SECRET_KEY) - slack_token = token_encryption.decrypt_token(slack_token) - logger.info(f"Decrypted Slack bot token for connector {connector_id}") - except Exception as e: - await task_logger.log_task_failure( - log_entry, - f"Failed to decrypt Slack token for connector {connector_id}: {e!s}", - "Token decryption failed", - {"error_type": "TokenDecryptionError"}, - ) - return 0, f"Failed to decrypt Slack token: {e!s}" - - # Initialize Slack client + # Initialize Slack client with auto-refresh support await task_logger.log_task_progress( log_entry, f"Initializing Slack client for connector {connector_id}", {"stage": "client_initialization"}, ) - slack_client = SlackHistory(token=slack_token) + # Use the new pattern with session and connector_id for auto-refresh + slack_client = SlackHistory( + session=session, connector_id=connector_id + ) # Handle 'undefined' string from frontend (treat as None) if start_date == "undefined" or start_date == "": @@ -167,7 +142,7 @@ async def index_slack_messages( # Get all channels try: - channels = slack_client.get_all_channels() + channels = await slack_client.get_all_channels() except Exception as e: await task_logger.log_task_failure( log_entry, @@ -216,7 +191,7 @@ async def index_slack_messages( continue # Get messages for this channel - messages, error = slack_client.get_history_by_date_range( + messages, error = await slack_client.get_history_by_date_range( channel_id=channel_id, start_date=start_date_str, end_date=end_date_str, @@ -249,7 +224,7 @@ async def index_slack_messages( ]: continue - formatted_msg = slack_client.format_message( + formatted_msg = await slack_client.format_message( msg, include_user_info=True ) formatted_messages.append(formatted_msg) From 186273291331b756f5c3dc0da816bb8eeb37905c Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Sun, 4 Jan 2026 02:47:50 +0530 Subject: [PATCH 16/75] refactor: improve error handling for Slack token refresh logic - Updated SlackHistory class to enforce the use of session and connector_id for token refresh, raising a ValueError for legacy token usage. - Simplified conditional checks for client initialization in SlackHistory. - Cleaned up unnecessary comments and whitespace in the codebase. --- .../app/connectors/slack_history.py | 20 +++++++++++-------- .../app/schemas/slack_auth_credentials.py | 1 - .../tasks/connector_indexers/slack_indexer.py | 4 +--- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/surfsense_backend/app/connectors/slack_history.py b/surfsense_backend/app/connectors/slack_history.py index 6a016394e..dbf43bb24 100644 --- a/surfsense_backend/app/connectors/slack_history.py +++ b/surfsense_backend/app/connectors/slack_history.py @@ -65,19 +65,23 @@ class SlackHistory: """ # If we have a direct token (backward compatibility), use it # Check if client was initialized with a token directly (not via credentials) - if self.client and self._session is None and self._connector_id is None: + if ( + self.client + and self._session is None + and self._connector_id is None + and self._credentials is None + ): # This means it was initialized with a direct token, extract it # WebClient stores token internally, we need to get it from the client # For backward compatibility, we'll use the client directly # But we can't easily extract the token, so we'll just use the client # In this case, we'll skip refresh logic - if self._credentials is None: - # This is the old pattern - just use the client as-is - # We can't extract token easily, so we'll raise an error - # asking to use the new pattern - raise ValueError( - "Cannot refresh token: Please use session and connector_id for auto-refresh support" - ) + # This is the old pattern - just use the client as-is + # We can't extract token easily, so we'll raise an error + # asking to use the new pattern + raise ValueError( + "Cannot refresh token: Please use session and connector_id for auto-refresh support" + ) # Load credentials from DB if not provided if self._credentials is None: diff --git a/surfsense_backend/app/schemas/slack_auth_credentials.py b/surfsense_backend/app/schemas/slack_auth_credentials.py index ad6a713ef..5148a0985 100644 --- a/surfsense_backend/app/schemas/slack_auth_credentials.py +++ b/surfsense_backend/app/schemas/slack_auth_credentials.py @@ -73,4 +73,3 @@ class SlackAuthCredentialsBase(BaseModel): if isinstance(v, datetime): return v if v.tzinfo else v.replace(tzinfo=UTC) return v - diff --git a/surfsense_backend/app/tasks/connector_indexers/slack_indexer.py b/surfsense_backend/app/tasks/connector_indexers/slack_indexer.py index c7a815634..dad64ad27 100644 --- a/surfsense_backend/app/tasks/connector_indexers/slack_indexer.py +++ b/surfsense_backend/app/tasks/connector_indexers/slack_indexer.py @@ -103,9 +103,7 @@ async def index_slack_messages( ) # Use the new pattern with session and connector_id for auto-refresh - slack_client = SlackHistory( - session=session, connector_id=connector_id - ) + slack_client = SlackHistory(session=session, connector_id=connector_id) # Handle 'undefined' string from frontend (treat as None) if start_date == "undefined" or start_date == "": From df23813f1cfa310be240e906b41a483ab9bc6204 Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Mon, 5 Jan 2026 14:21:39 +0530 Subject: [PATCH 17/75] feat: add Discord OAuth integration and connector routes - Introduced Discord OAuth support with new environment variables for client ID, client secret, and redirect URI. - Implemented Discord connector routes for OAuth flow, including authorization and callback handling. - Enhanced Discord connector to support both OAuth-based authentication and legacy bot token usage. - Updated Discord indexing logic to utilize OAuth credentials with auto-refresh capabilities. - Removed outdated Discord UI components and adjusted frontend logic to reflect the new integration. --- surfsense_backend/.env.example | 6 + surfsense_backend/app/config/__init__.py | 6 + .../app/connectors/discord_connector.py | 169 +++++- surfsense_backend/app/routes/__init__.py | 2 + .../app/routes/discord_add_connector_route.py | 509 ++++++++++++++++++ .../app/schemas/discord_auth_credentials.py | 76 +++ .../connector_indexers/discord_indexer.py | 131 +++-- surfsense_backend/app/utils/validators.py | 2 +- .../components/discord-connect-form.tsx | 409 -------------- .../connector-popup/connect-forms/index.tsx | 3 - .../components/discord-config.tsx | 84 +-- .../views/connector-connect-view.tsx | 1 - .../constants/connector-constants.ts | 13 +- 13 files changed, 878 insertions(+), 533 deletions(-) create mode 100644 surfsense_backend/app/routes/discord_add_connector_route.py create mode 100644 surfsense_backend/app/schemas/discord_auth_credentials.py delete mode 100644 surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/discord-connect-form.tsx diff --git a/surfsense_backend/.env.example b/surfsense_backend/.env.example index 2cacedc21..d2c667178 100644 --- a/surfsense_backend/.env.example +++ b/surfsense_backend/.env.example @@ -44,6 +44,12 @@ AIRTABLE_CLIENT_ID=your_airtable_client_id AIRTABLE_CLIENT_SECRET=your_airtable_client_secret AIRTABLE_REDIRECT_URI=http://localhost:8000/api/v1/auth/airtable/connector/callback +# Discord OAuth Configuration +DISCORD_CLIENT_ID=your_discord_client_id_here +DISCORD_CLIENT_SECRET=your_discord_client_secret_here +DISCORD_REDIRECT_URI=http://localhost:8000/api/v1/auth/discord/connector/callback +DISCORD_BOT_TOKEN=your_bot_token_from_developer_portal + # OAuth for Linear Connector LINEAR_CLIENT_ID=your_linear_client_id LINEAR_CLIENT_SECRET=your_linear_client_secret diff --git a/surfsense_backend/app/config/__init__.py b/surfsense_backend/app/config/__init__.py index f69d1c1a3..f65a94cc0 100644 --- a/surfsense_backend/app/config/__init__.py +++ b/surfsense_backend/app/config/__init__.py @@ -105,6 +105,12 @@ class Config: SLACK_CLIENT_SECRET = os.getenv("SLACK_CLIENT_SECRET") SLACK_REDIRECT_URI = os.getenv("SLACK_REDIRECT_URI") + # Discord OAuth + DISCORD_CLIENT_ID = os.getenv("DISCORD_CLIENT_ID") + DISCORD_CLIENT_SECRET = os.getenv("DISCORD_CLIENT_SECRET") + DISCORD_REDIRECT_URI = os.getenv("DISCORD_REDIRECT_URI") + DISCORD_BOT_TOKEN = os.getenv("DISCORD_BOT_TOKEN") + # LLM instances are now managed per-user through the LLMConfig system # Legacy environment variables removed in favor of user-specific configurations diff --git a/surfsense_backend/app/connectors/discord_connector.py b/surfsense_backend/app/connectors/discord_connector.py index 506b463a5..1e12cb9a4 100644 --- a/surfsense_backend/app/connectors/discord_connector.py +++ b/surfsense_backend/app/connectors/discord_connector.py @@ -3,7 +3,7 @@ Discord Connector A module for interacting with Discord's HTTP API to retrieve guilds, channels, and message history. -Requires a Discord bot token. +Supports both direct bot token and OAuth-based authentication with token refresh. """ import asyncio @@ -12,6 +12,14 @@ import logging import discord from discord.ext import commands +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.future import select + +from app.config import config +from app.db import SearchSourceConnector +from app.routes.discord_add_connector_route import refresh_discord_token +from app.schemas.discord_auth_credentials import DiscordAuthCredentialsBase +from app.utils.oauth_security import TokenEncryption logger = logging.getLogger(__name__) @@ -19,12 +27,21 @@ logger = logging.getLogger(__name__) class DiscordConnector(commands.Bot): """Class for retrieving guild, channel, and message history from Discord.""" - def __init__(self, token: str | None = None): + def __init__( + self, + token: str | None = None, + session: AsyncSession | None = None, + connector_id: int | None = None, + credentials: DiscordAuthCredentialsBase | None = None, + ): """ - Initialize the DiscordConnector with a bot token. + Initialize the DiscordConnector with a bot token or OAuth credentials. Args: - token (str): The Discord bot token. + token: Discord bot token (optional, for backward compatibility) + session: Database session for token refresh (optional) + connector_id: Connector ID for token refresh (optional) + credentials: Discord OAuth credentials (optional, will be loaded from DB if not provided) """ intents = discord.Intents.default() intents.guilds = True # Required to fetch guilds and channels @@ -34,7 +51,14 @@ class DiscordConnector(commands.Bot): super().__init__( command_prefix="!", intents=intents ) # command_prefix is required but not strictly used here - self.token = token + self._session = session + self._connector_id = connector_id + self._credentials = credentials + # For backward compatibility, if token is provided directly, use it + if token: + self.token = token + else: + self.token = None self._bot_task = None # Holds the async bot task self._is_running = False # Flag to track if the bot is running @@ -57,12 +81,143 @@ class DiscordConnector(commands.Bot): async def on_resumed(): logger.debug("Bot resumed connection to Discord gateway.") + async def _get_valid_token(self) -> str: + """ + Get valid Discord bot token, refreshing if needed. + + Returns: + Valid bot token + + Raises: + ValueError: If credentials are missing or invalid + Exception: If token refresh fails + """ + # If we have a direct token (backward compatibility), use it + if ( + self.token + and self._session is None + and self._connector_id is None + and self._credentials is None + ): + # This means it was initialized with a direct token, use it + return self.token + + # Load credentials from DB if not provided + if self._credentials is None: + if not self._session or not self._connector_id: + raise ValueError( + "Cannot load credentials: session and connector_id required" + ) + + result = await self._session.execute( + select(SearchSourceConnector).filter( + SearchSourceConnector.id == self._connector_id + ) + ) + connector = result.scalars().first() + + if not connector: + raise ValueError(f"Connector {self._connector_id} not found") + + config_data = connector.config.copy() + + # Decrypt credentials if they are encrypted + token_encrypted = config_data.get("_token_encrypted", False) + if token_encrypted and config.SECRET_KEY: + try: + token_encryption = TokenEncryption(config.SECRET_KEY) + + # Decrypt sensitive fields + if config_data.get("bot_token"): + config_data["bot_token"] = token_encryption.decrypt_token( + config_data["bot_token"] + ) + if config_data.get("refresh_token"): + config_data["refresh_token"] = token_encryption.decrypt_token( + config_data["refresh_token"] + ) + + logger.info( + f"Decrypted Discord credentials for connector {self._connector_id}" + ) + except Exception as e: + logger.error( + f"Failed to decrypt Discord credentials for connector {self._connector_id}: {e!s}" + ) + raise ValueError( + f"Failed to decrypt Discord credentials: {e!s}" + ) from e + + try: + self._credentials = DiscordAuthCredentialsBase.from_dict(config_data) + except Exception as e: + raise ValueError(f"Invalid Discord credentials: {e!s}") from e + + # Check if token is expired and refreshable + if self._credentials.is_expired and self._credentials.is_refreshable: + try: + logger.info( + f"Discord token expired for connector {self._connector_id}, refreshing..." + ) + + # Get connector for refresh + result = await self._session.execute( + select(SearchSourceConnector).filter( + SearchSourceConnector.id == self._connector_id + ) + ) + connector = result.scalars().first() + + if not connector: + raise RuntimeError( + f"Connector {self._connector_id} not found; cannot refresh token." + ) + + # Refresh token + connector = await refresh_discord_token(self._session, connector) + + # Reload credentials after refresh + config_data = connector.config.copy() + token_encrypted = config_data.get("_token_encrypted", False) + if token_encrypted and config.SECRET_KEY: + token_encryption = TokenEncryption(config.SECRET_KEY) + if config_data.get("bot_token"): + config_data["bot_token"] = token_encryption.decrypt_token( + config_data["bot_token"] + ) + if config_data.get("refresh_token"): + config_data["refresh_token"] = token_encryption.decrypt_token( + config_data["refresh_token"] + ) + + self._credentials = DiscordAuthCredentialsBase.from_dict(config_data) + + logger.info( + f"Successfully refreshed Discord token for connector {self._connector_id}" + ) + except Exception as e: + logger.error( + f"Failed to refresh Discord token for connector {self._connector_id}: {e!s}" + ) + raise Exception( + f"Failed to refresh Discord OAuth credentials: {e!s}" + ) from e + + return self._credentials.bot_token + async def start_bot(self): """Starts the bot to connect to Discord.""" logger.info("Starting Discord bot...") + # Get valid token (with auto-refresh if using OAuth) if not self.token: - raise ValueError("Discord bot token not set. Call set_token(token) first.") + # Try to get token from credentials + try: + self.token = await self._get_valid_token() + except ValueError as e: + raise ValueError( + f"Discord bot token not set. {e!s} Please authenticate via OAuth or provide a token." + ) from e try: if self._is_running: @@ -107,7 +262,7 @@ class DiscordConnector(commands.Bot): def set_token(self, token: str) -> None: """ - Set the discord bot token. + Set the discord bot token (for backward compatibility). Args: token (str): The Discord bot token. diff --git a/surfsense_backend/app/routes/__init__.py b/surfsense_backend/app/routes/__init__.py index 05020deff..b35d743e0 100644 --- a/surfsense_backend/app/routes/__init__.py +++ b/surfsense_backend/app/routes/__init__.py @@ -27,6 +27,7 @@ from .rbac_routes import router as rbac_router from .search_source_connectors_routes import router as search_source_connectors_router from .search_spaces_routes import router as search_spaces_router from .slack_add_connector_route import router as slack_add_connector_router +from .discord_add_connector_route import router as discord_add_connector_router router = APIRouter() @@ -46,6 +47,7 @@ router.include_router(linear_add_connector_router) router.include_router(luma_add_connector_router) router.include_router(notion_add_connector_router) router.include_router(slack_add_connector_router) +router.include_router(discord_add_connector_router) router.include_router(new_llm_config_router) # LLM configs with prompt configuration router.include_router(logs_router) router.include_router(circleback_webhook_router) # Circleback meeting webhooks diff --git a/surfsense_backend/app/routes/discord_add_connector_route.py b/surfsense_backend/app/routes/discord_add_connector_route.py new file mode 100644 index 000000000..70a0046a3 --- /dev/null +++ b/surfsense_backend/app/routes/discord_add_connector_route.py @@ -0,0 +1,509 @@ +""" +Discord Connector OAuth Routes. + +Handles OAuth 2.0 authentication flow for Discord connector. +""" + +import logging +from datetime import UTC, datetime, timedelta +from uuid import UUID + +import httpx +from fastapi import APIRouter, Depends, HTTPException, Request +from fastapi.responses import RedirectResponse +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, + SearchSourceConnectorType, + User, + get_async_session, +) +from app.schemas.discord_auth_credentials import DiscordAuthCredentialsBase +from app.users import current_active_user +from app.utils.oauth_security import OAuthStateManager, TokenEncryption + +logger = logging.getLogger(__name__) + +router = APIRouter() + +# Discord OAuth endpoints +AUTHORIZATION_URL = "https://discord.com/api/oauth2/authorize" +TOKEN_URL = "https://discord.com/api/oauth2/token" + +# OAuth scopes for Discord (Bot Token) +SCOPES = [ + "bot", # Basic bot scope + "guilds", # Access to guild information + "guilds.members.read", # Read member information +] + +# Initialize security utilities +_state_manager = None +_token_encryption = None + + +def get_state_manager() -> OAuthStateManager: + """Get or create OAuth state manager instance.""" + global _state_manager + if _state_manager is None: + if not config.SECRET_KEY: + raise ValueError("SECRET_KEY must be set for OAuth security") + _state_manager = OAuthStateManager(config.SECRET_KEY) + return _state_manager + + +def get_token_encryption() -> TokenEncryption: + """Get or create token encryption instance.""" + global _token_encryption + if _token_encryption is None: + if not config.SECRET_KEY: + raise ValueError("SECRET_KEY must be set for token encryption") + _token_encryption = TokenEncryption(config.SECRET_KEY) + return _token_encryption + + +@router.get("/auth/discord/connector/add") +async def connect_discord(space_id: int, user: User = Depends(current_active_user)): + """ + Initiate Discord OAuth flow. + + Args: + space_id: The search space ID + user: Current authenticated user + + Returns: + Authorization URL for redirect + """ + try: + if not space_id: + raise HTTPException(status_code=400, detail="space_id is required") + + if not config.DISCORD_CLIENT_ID: + raise HTTPException(status_code=500, detail="Discord OAuth not configured.") + + if not config.DISCORD_BOT_TOKEN: + raise HTTPException( + status_code=500, + detail="Discord bot token not configured. Please set DISCORD_BOT_TOKEN in backend configuration.", + ) + + if not config.SECRET_KEY: + raise HTTPException( + status_code=500, detail="SECRET_KEY not configured for OAuth security." + ) + + # Generate secure state parameter with HMAC signature + state_manager = get_state_manager() + state_encoded = state_manager.generate_secure_state(space_id, user.id) + + # Build authorization URL + from urllib.parse import urlencode + + auth_params = { + "client_id": config.DISCORD_CLIENT_ID, + "scope": " ".join(SCOPES), # Discord uses space-separated scopes + "redirect_uri": config.DISCORD_REDIRECT_URI, + "response_type": "code", + "state": state_encoded, + } + + auth_url = f"{AUTHORIZATION_URL}?{urlencode(auth_params)}" + + logger.info(f"Generated Discord OAuth URL for user {user.id}, space {space_id}") + return {"auth_url": auth_url} + + except Exception as e: + logger.error(f"Failed to initiate Discord OAuth: {e!s}", exc_info=True) + raise HTTPException( + status_code=500, detail=f"Failed to initiate Discord OAuth: {e!s}" + ) from e + + +@router.get("/auth/discord/connector/callback") +async def discord_callback( + request: Request, + code: str | None = None, + error: str | None = None, + state: str | None = None, + session: AsyncSession = Depends(get_async_session), +): + """ + Handle Discord OAuth callback. + + Args: + request: FastAPI request object + code: Authorization code from Discord (if user granted access) + error: Error code from Discord (if user denied access or error occurred) + state: State parameter containing user/space info + session: Database session + + Returns: + Redirect response to frontend + """ + try: + # Handle OAuth errors (e.g., user denied access) + if error: + logger.warning(f"Discord OAuth error: {error}") + # Try to decode state to get space_id for redirect, but don't fail if it's invalid + space_id = None + if state: + try: + state_manager = get_state_manager() + data = state_manager.validate_state(state) + space_id = data.get("space_id") + except Exception: + # If state is invalid, we'll redirect without space_id + logger.warning("Failed to validate state in error handler") + + # Redirect to frontend with error parameter + if space_id: + return RedirectResponse( + url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&error=discord_oauth_denied" + ) + else: + return RedirectResponse( + url=f"{config.NEXT_FRONTEND_URL}/dashboard?error=discord_oauth_denied" + ) + + # Validate required parameters for successful flow + if not code: + raise HTTPException(status_code=400, detail="Missing authorization code") + if not state: + raise HTTPException(status_code=400, detail="Missing state parameter") + + # Validate and decode state with signature verification + state_manager = get_state_manager() + try: + data = state_manager.validate_state(state) + except HTTPException: + raise + except Exception as e: + raise HTTPException( + status_code=400, detail=f"Invalid state parameter: {e!s}" + ) from e + + user_id = UUID(data["user_id"]) + space_id = data["space_id"] + + # Validate redirect URI (security: ensure it matches configured value) + if not config.DISCORD_REDIRECT_URI: + raise HTTPException( + status_code=500, detail="DISCORD_REDIRECT_URI not configured" + ) + + # Exchange authorization code for access token + token_data = { + "client_id": config.DISCORD_CLIENT_ID, + "client_secret": config.DISCORD_CLIENT_SECRET, + "grant_type": "authorization_code", + "code": code, + "redirect_uri": config.DISCORD_REDIRECT_URI, + } + + async with httpx.AsyncClient() as client: + token_response = await client.post( + TOKEN_URL, + data=token_data, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + timeout=30.0, + ) + + if token_response.status_code != 200: + error_detail = token_response.text + try: + error_json = token_response.json() + error_detail = error_json.get("error_description", error_json.get("error", error_detail)) + except Exception: + pass + raise HTTPException( + status_code=400, detail=f"Token exchange failed: {error_detail}" + ) + + token_json = token_response.json() + + # Log OAuth response for debugging (without sensitive data) + logger.info(f"Discord OAuth response received. Keys: {list(token_json.keys())}") + + # Discord OAuth with 'bot' scope returns access_token (user token), not bot token + # The bot token must come from backend config (DISCORD_BOT_TOKEN) + # OAuth is used to authorize bot installation to servers, not to get bot token + if not config.DISCORD_BOT_TOKEN: + raise HTTPException( + status_code=500, + detail="Discord bot token not configured. Please set DISCORD_BOT_TOKEN in backend configuration.", + ) + + # Use the bot token from backend config (not the OAuth access_token) + bot_token = config.DISCORD_BOT_TOKEN + + # Extract OAuth access_token and refresh_token (for reference, not used for bot operations) + oauth_access_token = token_json.get("access_token") + refresh_token = token_json.get("refresh_token") + + # Encrypt sensitive tokens before storing + token_encryption = get_token_encryption() + + # Calculate expiration time (UTC, tz-aware) + expires_at = None + if token_json.get("expires_in"): + now_utc = datetime.now(UTC) + expires_at = now_utc + timedelta(seconds=int(token_json["expires_in"])) + + # Extract guild info from OAuth response if available + guild_id = None + guild_name = None + if token_json.get("guild"): + guild_id = token_json["guild"].get("id") + guild_name = token_json["guild"].get("name") + + # Store the bot token from config and OAuth metadata + connector_config = { + "bot_token": token_encryption.encrypt_token(bot_token), # Use bot token from config + "oauth_access_token": token_encryption.encrypt_token(oauth_access_token) + if oauth_access_token + else None, # Store OAuth token for reference + "refresh_token": token_encryption.encrypt_token(refresh_token) + if refresh_token + else None, + "token_type": token_json.get("token_type", "Bearer"), + "expires_in": token_json.get("expires_in"), + "expires_at": expires_at.isoformat() if expires_at else None, + "scope": token_json.get("scope"), + "guild_id": guild_id, + "guild_name": guild_name, + # Mark that tokens are encrypted for backward compatibility + "_token_encrypted": True, + } + + # Check if connector already exists for this search space and user + existing_connector_result = await session.execute( + select(SearchSourceConnector).filter( + SearchSourceConnector.search_space_id == space_id, + SearchSourceConnector.user_id == user_id, + SearchSourceConnector.connector_type + == SearchSourceConnectorType.DISCORD_CONNECTOR, + ) + ) + existing_connector = existing_connector_result.scalars().first() + + if existing_connector: + # Update existing connector + existing_connector.config = connector_config + existing_connector.name = "Discord Connector" + existing_connector.is_indexable = True + logger.info( + f"Updated existing Discord connector for user {user_id} in space {space_id}" + ) + else: + # Create new connector + new_connector = SearchSourceConnector( + name="Discord Connector", + connector_type=SearchSourceConnectorType.DISCORD_CONNECTOR, + is_indexable=True, + config=connector_config, + search_space_id=space_id, + user_id=user_id, + ) + session.add(new_connector) + logger.info( + f"Created new Discord connector for user {user_id} in space {space_id}" + ) + + try: + await session.commit() + logger.info(f"Successfully saved Discord connector for user {user_id}") + + # Redirect to the frontend with success params + return RedirectResponse( + url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&success=true&connector=discord-connector" + ) + + except ValidationError as e: + await session.rollback() + raise HTTPException( + status_code=422, detail=f"Validation error: {e!s}" + ) from e + except IntegrityError as e: + await session.rollback() + raise HTTPException( + status_code=409, + detail=f"Integrity error: A connector with this type already exists. {e!s}", + ) from e + except Exception as e: + logger.error(f"Failed to create search source connector: {e!s}") + await session.rollback() + raise HTTPException( + status_code=500, + detail=f"Failed to create search source connector: {e!s}", + ) from e + + except HTTPException: + raise + except Exception as e: + logger.error(f"Failed to complete Discord OAuth: {e!s}", exc_info=True) + raise HTTPException( + status_code=500, detail=f"Failed to complete Discord OAuth: {e!s}" + ) from e + + +async def refresh_discord_token( + session: AsyncSession, connector: SearchSourceConnector +) -> SearchSourceConnector: + """ + Refresh the Discord OAuth tokens for a connector. + + Note: Bot tokens from config don't expire, but OAuth access tokens might. + This function refreshes OAuth tokens if needed, but always uses bot token from config. + + Args: + session: Database session + connector: Discord connector to refresh + + Returns: + Updated connector object + """ + try: + logger.info(f"Refreshing Discord OAuth tokens for connector {connector.id}") + + # Bot token always comes from config, not from OAuth + if not config.DISCORD_BOT_TOKEN: + raise HTTPException( + status_code=500, + detail="Discord bot token not configured. Please set DISCORD_BOT_TOKEN in backend configuration.", + ) + + credentials = DiscordAuthCredentialsBase.from_dict(connector.config) + + # Decrypt tokens if they are encrypted + token_encryption = get_token_encryption() + is_encrypted = connector.config.get("_token_encrypted", False) + + refresh_token = credentials.refresh_token + if is_encrypted and refresh_token: + try: + refresh_token = token_encryption.decrypt_token(refresh_token) + except Exception as e: + logger.error(f"Failed to decrypt refresh token: {e!s}") + raise HTTPException( + status_code=500, detail="Failed to decrypt stored refresh token" + ) from e + + # If no refresh token, bot token from config is still valid (bot tokens don't expire) + # Just update the bot token from config in case it was changed + if not refresh_token: + logger.info( + f"No refresh token available for connector {connector.id}. Using bot token from config." + ) + # Update bot token from config (in case it was changed) + credentials.bot_token = token_encryption.encrypt_token(config.DISCORD_BOT_TOKEN) + credentials_dict = credentials.to_dict() + credentials_dict["_token_encrypted"] = True + connector.config = credentials_dict + await session.commit() + await session.refresh(connector) + return connector + + # Discord uses oauth2/token for token refresh with grant_type=refresh_token + refresh_data = { + "client_id": config.DISCORD_CLIENT_ID, + "client_secret": config.DISCORD_CLIENT_SECRET, + "grant_type": "refresh_token", + "refresh_token": refresh_token, + } + + async with httpx.AsyncClient() as client: + token_response = await client.post( + TOKEN_URL, + data=refresh_data, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + timeout=30.0, + ) + + if token_response.status_code != 200: + error_detail = token_response.text + try: + error_json = token_response.json() + error_detail = error_json.get("error_description", error_json.get("error", error_detail)) + except Exception: + pass + # If refresh fails, bot token from config is still valid + logger.warning( + f"OAuth token refresh failed for connector {connector.id}: {error_detail}. " + "Using bot token from config." + ) + # Update bot token from config + credentials.bot_token = token_encryption.encrypt_token(config.DISCORD_BOT_TOKEN) + credentials.refresh_token = None # Clear invalid refresh token + credentials_dict = credentials.to_dict() + credentials_dict["_token_encrypted"] = True + connector.config = credentials_dict + await session.commit() + await session.refresh(connector) + return connector + + token_json = token_response.json() + + # Extract OAuth access token from refresh response (for reference) + oauth_access_token = token_json.get("access_token") + + # Get new refresh token if provided (Discord may rotate refresh tokens) + new_refresh_token = token_json.get("refresh_token") + + # Calculate expiration time (UTC, tz-aware) + expires_at = None + expires_in = token_json.get("expires_in") + if expires_in: + now_utc = datetime.now(UTC) + expires_at = now_utc + timedelta(seconds=int(expires_in)) + + # Always use bot token from config (bot tokens don't expire) + credentials.bot_token = token_encryption.encrypt_token(config.DISCORD_BOT_TOKEN) + + # Update OAuth tokens if available + if oauth_access_token: + # Store OAuth access token for reference + connector.config["oauth_access_token"] = token_encryption.encrypt_token( + oauth_access_token + ) + if new_refresh_token: + credentials.refresh_token = token_encryption.encrypt_token( + new_refresh_token + ) + credentials.expires_in = expires_in + credentials.expires_at = expires_at + credentials.scope = token_json.get("scope") + + # Preserve guild info if present + if not credentials.guild_id: + credentials.guild_id = connector.config.get("guild_id") + if not credentials.guild_name: + credentials.guild_name = connector.config.get("guild_name") + if not credentials.bot_user_id: + credentials.bot_user_id = connector.config.get("bot_user_id") + + # Update connector config with encrypted tokens + credentials_dict = credentials.to_dict() + credentials_dict["_token_encrypted"] = True + connector.config = credentials_dict + await session.commit() + await session.refresh(connector) + + logger.info(f"Successfully refreshed Discord OAuth tokens for connector {connector.id}") + + return connector + except HTTPException: + raise + except Exception as e: + logger.error( + f"Failed to refresh Discord tokens for connector {connector.id}: {e!s}", + exc_info=True, + ) + raise HTTPException( + status_code=500, detail=f"Failed to refresh Discord tokens: {e!s}" + ) from e + diff --git a/surfsense_backend/app/schemas/discord_auth_credentials.py b/surfsense_backend/app/schemas/discord_auth_credentials.py new file mode 100644 index 000000000..0c18a7554 --- /dev/null +++ b/surfsense_backend/app/schemas/discord_auth_credentials.py @@ -0,0 +1,76 @@ +from datetime import UTC, datetime + +from pydantic import BaseModel, field_validator + + +class DiscordAuthCredentialsBase(BaseModel): + bot_token: str + refresh_token: str | None = None + token_type: str = "Bearer" + expires_in: int | None = None + expires_at: datetime | None = None + scope: str | None = None + bot_user_id: str | None = None + guild_id: str | None = None + guild_name: str | None = None + + @property + def is_expired(self) -> bool: + """Check if the credentials have expired.""" + if self.expires_at is None: + return False # Long-lived token, treat as not expired + return self.expires_at <= datetime.now(UTC) + + @property + def is_refreshable(self) -> bool: + """Check if the credentials can be refreshed.""" + return self.refresh_token is not None + + def to_dict(self) -> dict: + """Convert credentials to dictionary for storage.""" + return { + "bot_token": self.bot_token, + "refresh_token": self.refresh_token, + "token_type": self.token_type, + "expires_in": self.expires_in, + "expires_at": self.expires_at.isoformat() if self.expires_at else None, + "scope": self.scope, + "bot_user_id": self.bot_user_id, + "guild_id": self.guild_id, + "guild_name": self.guild_name, + } + + @classmethod + def from_dict(cls, data: dict) -> "DiscordAuthCredentialsBase": + """Create credentials from dictionary.""" + expires_at = None + if data.get("expires_at"): + expires_at = datetime.fromisoformat(data["expires_at"]) + + return cls( + bot_token=data.get("bot_token", ""), + refresh_token=data.get("refresh_token"), + token_type=data.get("token_type", "Bearer"), + expires_in=data.get("expires_in"), + expires_at=expires_at, + scope=data.get("scope"), + bot_user_id=data.get("bot_user_id"), + guild_id=data.get("guild_id"), + guild_name=data.get("guild_name"), + ) + + @field_validator("expires_at", mode="before") + @classmethod + def ensure_aware_utc(cls, v): + # Strings like "2025-08-26T14:46:57.367184" + if isinstance(v, str): + # add +00:00 if missing tz info + if v.endswith("Z"): + return datetime.fromisoformat(v.replace("Z", "+00:00")) + dt = datetime.fromisoformat(v) + return dt if dt.tzinfo else dt.replace(tzinfo=UTC) + # datetime objects + if isinstance(v, datetime): + return v if v.tzinfo else v.replace(tzinfo=UTC) + return v + diff --git a/surfsense_backend/app/tasks/connector_indexers/discord_indexer.py b/surfsense_backend/app/tasks/connector_indexers/discord_indexer.py index 9391be788..b3de1f4b5 100644 --- a/surfsense_backend/app/tasks/connector_indexers/discord_indexer.py +++ b/surfsense_backend/app/tasks/connector_indexers/discord_indexer.py @@ -8,6 +8,7 @@ from datetime import UTC, datetime, timedelta from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.ext.asyncio import AsyncSession +from app.config import config from app.connectors.discord_connector import DiscordConnector from app.db import Document, DocumentType, SearchSourceConnectorType from app.services.llm_service import get_user_long_context_llm @@ -69,6 +70,12 @@ async def index_discord_messages( ) try: + # Normalize date parameters - handle 'undefined' strings from frontend + if start_date and (start_date.lower() == "undefined" or start_date.strip() == ""): + start_date = None + if end_date and (end_date.lower() == "undefined" or end_date.strip() == ""): + end_date = None + # Get the connector await task_logger.log_task_progress( log_entry, @@ -92,27 +99,54 @@ async def index_discord_messages( f"Connector with ID {connector_id} not found or is not a Discord connector", ) - # Get the Discord token from the connector config - discord_token = connector.config.get("DISCORD_BOT_TOKEN") - if not discord_token: - await task_logger.log_task_failure( - log_entry, - f"Discord token not found in connector config for connector {connector_id}", - "Missing Discord token", - {"error_type": "MissingToken"}, - ) - return 0, "Discord token not found in connector config" - logger.info(f"Starting Discord indexing for connector {connector_id}") - # Initialize Discord client + # Initialize Discord client with OAuth credentials support await task_logger.log_task_progress( log_entry, f"Initializing Discord client for connector {connector_id}", {"stage": "client_initialization"}, ) - discord_client = DiscordConnector(token=discord_token) + # Check if using OAuth (has bot_token in config) or legacy (has DISCORD_BOT_TOKEN) + has_oauth = connector.config.get("bot_token") is not None + has_legacy = connector.config.get("DISCORD_BOT_TOKEN") is not None + + if has_oauth: + # Use OAuth credentials with auto-refresh + discord_client = DiscordConnector( + session=session, connector_id=connector_id + ) + elif has_legacy: + # Backward compatibility: use legacy token format + discord_token = connector.config.get("DISCORD_BOT_TOKEN") + + # Decrypt token if it's encrypted (legacy tokens might be encrypted) + token_encrypted = connector.config.get("_token_encrypted", False) + if token_encrypted and config.SECRET_KEY and discord_token: + try: + from app.utils.oauth_security import TokenEncryption + token_encryption = TokenEncryption(config.SECRET_KEY) + discord_token = token_encryption.decrypt_token(discord_token) + logger.info( + f"Decrypted legacy Discord token for connector {connector_id}" + ) + except Exception as e: + logger.warning( + f"Failed to decrypt legacy Discord token for connector {connector_id}: {e!s}. " + "Trying to use token as-is (might be unencrypted)." + ) + # Continue with token as-is - might be unencrypted legacy token + + discord_client = DiscordConnector(token=discord_token) + else: + await task_logger.log_task_failure( + log_entry, + f"Discord credentials not found in connector config for connector {connector_id}", + "Missing Discord credentials", + {"error_type": "MissingCredentials"}, + ) + return 0, "Discord credentials not found in connector config" # Calculate date range if start_date is None or end_date is None: @@ -135,32 +169,63 @@ async def index_discord_messages( if start_date is None: start_date_iso = calculated_start_date.isoformat() else: - # Convert YYYY-MM-DD to ISO format + # Validate and convert YYYY-MM-DD to ISO format + try: + start_date_iso = ( + datetime.strptime(start_date, "%Y-%m-%d") + .replace(tzinfo=UTC) + .isoformat() + ) + except ValueError as e: + logger.warning( + f"Invalid start_date format '{start_date}', using calculated start date: {e!s}" + ) + start_date_iso = calculated_start_date.isoformat() + + if end_date is None: + end_date_iso = calculated_end_date.isoformat() + else: + # Validate and convert YYYY-MM-DD to ISO format + try: + end_date_iso = ( + datetime.strptime(end_date, "%Y-%m-%d") + .replace(tzinfo=UTC) + .isoformat() + ) + except ValueError as e: + logger.warning( + f"Invalid end_date format '{end_date}', using calculated end date: {e!s}" + ) + end_date_iso = calculated_end_date.isoformat() + else: + # Convert provided dates to ISO format for Discord API + try: start_date_iso = ( datetime.strptime(start_date, "%Y-%m-%d") .replace(tzinfo=UTC) .isoformat() ) - - if end_date is None: - end_date_iso = calculated_end_date.isoformat() - else: - # Convert YYYY-MM-DD to ISO format - end_date_iso = ( - datetime.strptime(end_date, "%Y-%m-%d") - .replace(tzinfo=UTC) - .isoformat() + except ValueError as e: + await task_logger.log_task_failure( + log_entry, + f"Invalid start_date format: {start_date}", + f"Date parsing error: {e!s}", + {"error_type": "InvalidDateFormat", "start_date": start_date}, ) - else: - # Convert provided dates to ISO format for Discord API - start_date_iso = ( - datetime.strptime(start_date, "%Y-%m-%d") - .replace(tzinfo=UTC) - .isoformat() - ) - end_date_iso = ( - datetime.strptime(end_date, "%Y-%m-%d").replace(tzinfo=UTC).isoformat() - ) + return 0, f"Invalid start_date format: {start_date}. Expected YYYY-MM-DD format." + + try: + end_date_iso = ( + datetime.strptime(end_date, "%Y-%m-%d").replace(tzinfo=UTC).isoformat() + ) + except ValueError as e: + await task_logger.log_task_failure( + log_entry, + f"Invalid end_date format: {end_date}", + f"Date parsing error: {e!s}", + {"error_type": "InvalidDateFormat", "end_date": end_date}, + ) + return 0, f"Invalid end_date format: {end_date}. Expected YYYY-MM-DD format." logger.info( f"Indexing Discord messages from {start_date_iso} to {end_date_iso}" diff --git a/surfsense_backend/app/utils/validators.py b/surfsense_backend/app/utils/validators.py index 8db6ed4a3..f1620c0e5 100644 --- a/surfsense_backend/app/utils/validators.py +++ b/surfsense_backend/app/utils/validators.py @@ -537,7 +537,7 @@ def validate_connector_config( ) }, }, - "DISCORD_CONNECTOR": {"required": ["DISCORD_BOT_TOKEN"], "validators": {}}, + # "DISCORD_CONNECTOR": {"required": ["DISCORD_BOT_TOKEN"], "validators": {}}, "JIRA_CONNECTOR": { "required": ["JIRA_EMAIL", "JIRA_API_TOKEN", "JIRA_BASE_URL"], "validators": { diff --git a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/discord-connect-form.tsx b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/discord-connect-form.tsx deleted file mode 100644 index 8f4fa1a47..000000000 --- a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/discord-connect-form.tsx +++ /dev/null @@ -1,409 +0,0 @@ -"use client"; - -import { zodResolver } from "@hookform/resolvers/zod"; -import { Info } from "lucide-react"; -import type { FC } from "react"; -import { useRef, useState } from "react"; -import { useForm } from "react-hook-form"; -import * as z from "zod"; -import { - Accordion, - AccordionContent, - AccordionItem, - AccordionTrigger, -} from "@/components/ui/accordion"; -import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; -import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { Switch } from "@/components/ui/switch"; -import { EnumConnectorName } from "@/contracts/enums/connector"; -import { DateRangeSelector } from "../../components/date-range-selector"; -import { getConnectorBenefits } from "../connector-benefits"; -import type { ConnectFormProps } from "../index"; - -const discordConnectorFormSchema = z.object({ - name: z.string().min(3, { - message: "Connector name must be at least 3 characters.", - }), - bot_token: z.string().min(10, { - message: "Discord Bot Token is required and must be valid.", - }), -}); - -type DiscordConnectorFormValues = z.infer; - -export const DiscordConnectForm: FC = ({ onSubmit, isSubmitting }) => { - const isSubmittingRef = useRef(false); - const [startDate, setStartDate] = useState(undefined); - const [endDate, setEndDate] = useState(undefined); - const [periodicEnabled, setPeriodicEnabled] = useState(false); - const [frequencyMinutes, setFrequencyMinutes] = useState("1440"); - const form = useForm({ - resolver: zodResolver(discordConnectorFormSchema), - defaultValues: { - name: "Discord Connector", - bot_token: "", - }, - }); - - const handleSubmit = async (values: DiscordConnectorFormValues) => { - // Prevent multiple submissions - if (isSubmittingRef.current || isSubmitting) { - return; - } - - isSubmittingRef.current = true; - try { - await onSubmit({ - name: values.name, - connector_type: EnumConnectorName.DISCORD_CONNECTOR, - config: { - DISCORD_BOT_TOKEN: values.bot_token, - }, - is_indexable: true, - last_indexed_at: null, - periodic_indexing_enabled: periodicEnabled, - indexing_frequency_minutes: periodicEnabled ? parseInt(frequencyMinutes, 10) : null, - next_scheduled_at: null, - startDate, - endDate, - periodicEnabled, - frequencyMinutes, - }); - } finally { - isSubmittingRef.current = false; - } - }; - - return ( -
- - -
- Bot Token Required - - You'll need a Discord Bot Token to use this connector. You can create one from{" "} - - Discord Developer Portal - - -
-
- -
-
- - ( - - Connector Name - - - - - A friendly name to identify this connector. - - - - )} - /> - - ( - - Discord Bot Token - - - - - Your Discord Bot Token will be encrypted and stored securely. - - - - )} - /> - - {/* Indexing Configuration */} -
-

Indexing Configuration

- - {/* Date Range Selector */} - - - {/* Periodic Sync Config */} -
-
-
-

Enable Periodic Sync

-

- Automatically re-index at regular intervals -

-
- -
- - {periodicEnabled && ( -
-
- - -
-
- )} -
-
- - -
- - {/* What you get section */} - {getConnectorBenefits(EnumConnectorName.DISCORD_CONNECTOR) && ( -
-

What you get with Discord integration:

-
    - {getConnectorBenefits(EnumConnectorName.DISCORD_CONNECTOR)?.map((benefit) => ( -
  • {benefit}
  • - ))} -
-
- )} - - {/* Documentation Section */} - - - - Documentation - - -
-

How it works

-

- The Discord connector uses the Discord API to fetch messages from all accessible - channels that the bot token has access to within a server. -

-
    -
  • - For follow up indexing runs, the connector retrieves messages that have been - updated since the last indexing attempt. -
  • -
  • - Indexing is configured to run periodically, so updates should appear in your - search results within minutes. -
  • -
-
- -
-
-

Authorization

- - - Bot Token Required - - You need to create a Discord application and bot to get a bot token. The bot - needs read access to channels and messages. - - - -
-
-

- Step 1: Create a Discord Application -

-
    -
  1. - Go to{" "} - - https://discord.com/developers/applications - -
  2. -
  3. - Click New Application -
  4. -
  5. - Enter an application name and click Create -
  6. -
-
- -
-

- Step 2: Create a Bot -

-
    -
  1. - Navigate to Bot in the sidebar -
  2. -
  3. - Click Add Bot and confirm -
  4. -
  5. - Under Privileged Gateway Intents, enable: -
      -
    • - - MESSAGE CONTENT INTENT - {" "} - - Required to read message content -
    • -
    -
  6. -
-
- -
-

- Step 3: Get Bot Token and Invite Bot -

-
    -
  1. - Under Token, click Reset Token and copy - the token -
  2. -
  3. - Navigate to OAuth2 → URL Generator -
  4. -
  5. - Select bot scope and Read Messages{" "} - permission -
  6. -
  7. Copy the generated URL and open it in your browser
  8. -
  9. Select your server and authorize the bot
  10. -
-
-
-
-
- -
-
-

Indexing

-
    -
  1. - Navigate to the Connector Dashboard and select the Discord{" "} - Connector. -
  2. -
  3. - Place the Bot Token in the form field. -
  4. -
  5. - Click Connect to establish the connection. -
  6. -
  7. Once connected, your Discord messages will be indexed automatically.
  8. -
- - - - What Gets Indexed - -

The Discord connector indexes the following data:

-
    -
  • Messages from all accessible channels
  • -
  • Direct messages (if bot has access)
  • -
  • Message timestamps and metadata
  • -
  • Thread replies and conversations
  • -
-
-
-
-
-
-
-
-
- ); -}; diff --git a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/index.tsx b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/index.tsx index 807d4cb7a..81e5ee03f 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/index.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/index.tsx @@ -4,7 +4,6 @@ import { BookStackConnectForm } from "./components/bookstack-connect-form"; import { CirclebackConnectForm } from "./components/circleback-connect-form"; import { ClickUpConnectForm } from "./components/clickup-connect-form"; import { ConfluenceConnectForm } from "./components/confluence-connect-form"; -import { DiscordConnectForm } from "./components/discord-connect-form"; import { ElasticsearchConnectForm } from "./components/elasticsearch-connect-form"; import { GithubConnectForm } from "./components/github-connect-form"; import { JiraConnectForm } from "./components/jira-connect-form"; @@ -50,8 +49,6 @@ export function getConnectFormComponent(connectorType: string): ConnectFormCompo return BaiduSearchApiConnectForm; case "ELASTICSEARCH_CONNECTOR": return ElasticsearchConnectForm; - case "DISCORD_CONNECTOR": - return DiscordConnectForm; case "CONFLUENCE_CONNECTOR": return ConfluenceConnectForm; case "BOOKSTACK_CONNECTOR": diff --git a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/discord-config.tsx b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/discord-config.tsx index 377987637..464bc438f 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/discord-config.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/discord-config.tsx @@ -1,88 +1,26 @@ "use client"; -import { KeyRound } from "lucide-react"; +import { Info } from "lucide-react"; import type { FC } from "react"; -import { useEffect, useState } from "react"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; import type { ConnectorConfigProps } from "../index"; export interface DiscordConfigProps extends ConnectorConfigProps { onNameChange?: (name: string) => void; } -export const DiscordConfig: FC = ({ - connector, - onConfigChange, - onNameChange, -}) => { - const [botToken, setBotToken] = useState( - (connector.config?.DISCORD_BOT_TOKEN as string) || "" - ); - const [name, setName] = useState(connector.name || ""); - - // Update bot token and name when connector changes - useEffect(() => { - const token = (connector.config?.DISCORD_BOT_TOKEN as string) || ""; - setBotToken(token); - setName(connector.name || ""); - }, [connector.config, connector.name]); - - const handleBotTokenChange = (value: string) => { - setBotToken(value); - if (onConfigChange) { - onConfigChange({ - ...connector.config, - DISCORD_BOT_TOKEN: value, - }); - } - }; - - const handleNameChange = (value: string) => { - setName(value); - if (onNameChange) { - onNameChange(value); - } - }; - +export const DiscordConfig: FC = () => { return (
- {/* Connector Name */} -
-
- - handleNameChange(e.target.value)} - placeholder="My Discord Connector" - className="border-slate-400/20 focus-visible:border-slate-400/40" - /> -

- A friendly name to identify this connector. -

+
+
+
-
- - {/* Configuration */} -
-
-

Configuration

-
- -
- - handleBotTokenChange(e.target.value)} - placeholder="Your Bot Token" - className="border-slate-400/20 focus-visible:border-slate-400/40" - /> -

- Update your Discord Bot Token if needed. +

+

Add Bot to Servers

+

+ Before indexing, make sure the Discord bot has been added to the servers (guilds) you want to + index. The bot can only access messages from servers it's been added to. Use the OAuth + authorization flow to add the bot to your servers.

diff --git a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-connect-view.tsx b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-connect-view.tsx index 5437426c8..3ba03f956 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-connect-view.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-connect-view.tsx @@ -52,7 +52,6 @@ export const ConnectorConnectView: FC = ({ LINKUP_API: "linkup-api-connect-form", BAIDU_SEARCH_API: "baidu-search-api-connect-form", ELASTICSEARCH_CONNECTOR: "elasticsearch-connect-form", - DISCORD_CONNECTOR: "discord-connect-form", CONFLUENCE_CONNECTOR: "confluence-connect-form", BOOKSTACK_CONNECTOR: "bookstack-connect-form", GITHUB_CONNECTOR: "github-connect-form", diff --git a/surfsense_web/components/assistant-ui/connector-popup/constants/connector-constants.ts b/surfsense_web/components/assistant-ui/connector-popup/constants/connector-constants.ts index 111b7485d..9822ff6e6 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/constants/connector-constants.ts +++ b/surfsense_web/components/assistant-ui/connector-popup/constants/connector-constants.ts @@ -51,6 +51,13 @@ export const OAUTH_CONNECTORS = [ connectorType: EnumConnectorName.SLACK_CONNECTOR, authEndpoint: "/api/v1/auth/slack/connector/add/", }, + { + id: "discord-connector", + title: "Discord", + description: "Search Discord messages", + connectorType: EnumConnectorName.DISCORD_CONNECTOR, + authEndpoint: "/api/v1/auth/discord/connector/add/", + }, ] as const; // Content Sources (tools that extract and import content from external sources) @@ -71,12 +78,6 @@ export const CRAWLERS = [ // Non-OAuth Connectors (redirect to old connector config pages) export const OTHER_CONNECTORS = [ - { - id: "discord-connector", - title: "Discord", - description: "Search Discord messages", - connectorType: EnumConnectorName.DISCORD_CONNECTOR, - }, { id: "confluence-connector", title: "Confluence", From bfed9a31f822929cc2b59b6c78a6ebe58a6c97d8 Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Mon, 5 Jan 2026 23:59:16 +0530 Subject: [PATCH 18/75] feat: implement Jira OAuth integration and connector routes - Added support for Jira OAuth with new environment variables for client ID, client secret, and redirect URI. - Implemented Jira connector routes for OAuth flow, including authorization and callback handling. - Enhanced Jira connector to support both OAuth 2.0 and legacy API token authentication. - Updated Jira indexing logic to utilize OAuth credentials with auto-refresh capabilities. - Removed outdated Jira UI components and adjusted frontend logic to reflect the new integration. --- surfsense_backend/.env.example | 5 + surfsense_backend/app/config/__init__.py | 5 + .../app/connectors/jira_connector.py | 103 +++- surfsense_backend/app/routes/__init__.py | 2 + .../app/routes/jira_add_connector_route.py | 495 ++++++++++++++++++ .../app/schemas/jira_auth_credentials.py | 73 +++ .../tasks/connector_indexers/jira_indexer.py | 142 ++++- surfsense_backend/app/utils/validators.py | 14 +- .../components/jira-connect-form.tsx | 450 ---------------- .../connector-popup/connect-forms/index.tsx | 3 - .../components/jira-config.tsx | 51 +- .../views/connector-connect-view.tsx | 1 - .../constants/connector-constants.ts | 13 +- .../hooks/use-connector-edit-page.ts | 10 + 14 files changed, 845 insertions(+), 522 deletions(-) create mode 100644 surfsense_backend/app/routes/jira_add_connector_route.py create mode 100644 surfsense_backend/app/schemas/jira_auth_credentials.py delete mode 100644 surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/jira-connect-form.tsx diff --git a/surfsense_backend/.env.example b/surfsense_backend/.env.example index d2c667178..a2f662c23 100644 --- a/surfsense_backend/.env.example +++ b/surfsense_backend/.env.example @@ -50,6 +50,11 @@ DISCORD_CLIENT_SECRET=your_discord_client_secret_here DISCORD_REDIRECT_URI=http://localhost:8000/api/v1/auth/discord/connector/callback DISCORD_BOT_TOKEN=your_bot_token_from_developer_portal +# Jira OAuth Configuration +JIRA_CLIENT_ID=our_jira_client_id +JIRA_CLIENT_SECRET=your_jira_client_secret +JIRA_REDIRECT_URI=http://localhost:8000/api/v1/auth/jira/connector/callback + # OAuth for Linear Connector LINEAR_CLIENT_ID=your_linear_client_id LINEAR_CLIENT_SECRET=your_linear_client_secret diff --git a/surfsense_backend/app/config/__init__.py b/surfsense_backend/app/config/__init__.py index f65a94cc0..56641215d 100644 --- a/surfsense_backend/app/config/__init__.py +++ b/surfsense_backend/app/config/__init__.py @@ -111,6 +111,11 @@ class Config: DISCORD_REDIRECT_URI = os.getenv("DISCORD_REDIRECT_URI") DISCORD_BOT_TOKEN = os.getenv("DISCORD_BOT_TOKEN") + # Jira OAuth + JIRA_CLIENT_ID = os.getenv("JIRA_CLIENT_ID") + JIRA_CLIENT_SECRET = os.getenv("JIRA_CLIENT_SECRET") + JIRA_REDIRECT_URI = os.getenv("JIRA_REDIRECT_URI") + # LLM instances are now managed per-user through the LLMConfig system # Legacy environment variables removed in favor of user-specific configurations diff --git a/surfsense_backend/app/connectors/jira_connector.py b/surfsense_backend/app/connectors/jira_connector.py index e73198e79..8e9badf0b 100644 --- a/surfsense_backend/app/connectors/jira_connector.py +++ b/surfsense_backend/app/connectors/jira_connector.py @@ -3,6 +3,7 @@ Jira Connector Module A module for retrieving data from Jira. Allows fetching issue lists and their comments, projects and more. +Supports both OAuth 2.0 (preferred) and legacy API token authentication. """ import base64 @@ -18,6 +19,8 @@ class JiraConnector: def __init__( self, base_url: str | None = None, + access_token: str | None = None, + cloud_id: str | None = None, email: str | None = None, api_token: str | None = None, ): @@ -25,18 +28,39 @@ class JiraConnector: Initialize the JiraConnector class. Args: - base_url: Jira instance base URL (e.g., 'https://yourcompany.atlassian.net') (optional) - email: Jira account email address (optional) - api_token: Jira API token (optional) + base_url: Jira instance base URL (e.g., 'https://yourcompany.atlassian.net') + access_token: OAuth 2.0 access token (preferred method) + cloud_id: Atlassian cloud ID (used with OAuth for API URL construction) + email: Jira account email address (legacy method, used with api_token) + api_token: Jira API token (legacy method, used with email) """ self.base_url = base_url.rstrip("/") if base_url else None + self.access_token = access_token + self.cloud_id = cloud_id self.email = email self.api_token = api_token self.api_version = "3" # Jira Cloud API version + self._use_oauth = access_token is not None + + def set_oauth_credentials( + self, base_url: str, access_token: str, cloud_id: str | None = None + ) -> None: + """ + Set OAuth 2.0 credentials (preferred method). + + Args: + base_url: Jira instance base URL + access_token: OAuth 2.0 access token + cloud_id: Atlassian cloud ID (optional, used for API URL construction) + """ + self.base_url = base_url.rstrip("/") + self.access_token = access_token + self.cloud_id = cloud_id + self._use_oauth = True def set_credentials(self, base_url: str, email: str, api_token: str) -> None: """ - Set the Jira credentials. + Set the Jira credentials (legacy method using API token). Args: base_url: Jira instance base URL @@ -46,50 +70,69 @@ class JiraConnector: self.base_url = base_url.rstrip("/") self.email = email self.api_token = api_token + self._use_oauth = False def set_email(self, email: str) -> None: """ - Set the Jira account email. + Set the Jira account email (legacy method). Args: email: Jira account email address """ self.email = email + self._use_oauth = False def set_api_token(self, api_token: str) -> None: """ - Set the Jira API token. + Set the Jira API token (legacy method). Args: api_token: Jira API token """ self.api_token = api_token + self._use_oauth = False def get_headers(self) -> dict[str, str]: """ - Get headers for Jira API requests using Basic Authentication. + Get headers for Jira API requests. + + Uses OAuth Bearer token if available, otherwise falls back to Basic Auth. Returns: Dictionary of headers Raises: - ValueError: If email, api_token, or base_url have not been set + ValueError: If credentials have not been set """ - if not all([self.base_url, self.email, self.api_token]): - raise ValueError( - "Jira credentials not initialized. Call set_credentials() first." - ) + if self._use_oauth: + # OAuth 2.0 authentication + if not self.base_url or not self.access_token: + raise ValueError( + "Jira OAuth credentials not initialized. Call set_oauth_credentials() first." + ) - # Create Basic Auth header using email:api_token - auth_str = f"{self.email}:{self.api_token}" - auth_bytes = auth_str.encode("utf-8") - auth_header = "Basic " + base64.b64encode(auth_bytes).decode("ascii") + return { + "Content-Type": "application/json", + "Authorization": f"Bearer {self.access_token}", + "Accept": "application/json", + } + else: + # Legacy Basic Auth + if not all([self.base_url, self.email, self.api_token]): + raise ValueError( + "Jira credentials not initialized. Call set_credentials() first." + ) - return { - "Content-Type": "application/json", - "Authorization": auth_header, - "Accept": "application/json", - } + # Create Basic Auth header using email:api_token + auth_str = f"{self.email}:{self.api_token}" + auth_bytes = auth_str.encode("utf-8") + auth_header = "Basic " + base64.b64encode(auth_bytes).decode("ascii") + + return { + "Content-Type": "application/json", + "Authorization": auth_header, + "Accept": "application/json", + } def make_api_request( self, @@ -104,22 +147,26 @@ class JiraConnector: Args: endpoint: API endpoint (without base URL) params: Query parameters for the request (optional) + method: HTTP method (GET or POST) + json_payload: JSON payload for POST requests (optional) Returns: Response data from the API Raises: - ValueError: If email, api_token, or base_url have not been set + ValueError: If credentials have not been set Exception: If the API request fails """ - if not all([self.base_url, self.email, self.api_token]): - raise ValueError( - "Jira credentials not initialized. Call set_credentials() first." - ) - - url = f"{self.base_url}/rest/api/{self.api_version}/{endpoint}" headers = self.get_headers() + # Construct API URL based on authentication method + if self._use_oauth and self.cloud_id: + # Use Atlassian API gateway with cloud_id for OAuth + url = f"https://api.atlassian.com/ex/jira/{self.cloud_id}/rest/api/{self.api_version}/{endpoint}" + else: + # Use direct base URL (works for both OAuth and legacy) + url = f"{self.base_url}/rest/api/{self.api_version}/{endpoint}" + if method.upper() == "POST": response = requests.post( url, headers=headers, json=json_payload, timeout=500 diff --git a/surfsense_backend/app/routes/__init__.py b/surfsense_backend/app/routes/__init__.py index b35d743e0..16cacfeb8 100644 --- a/surfsense_backend/app/routes/__init__.py +++ b/surfsense_backend/app/routes/__init__.py @@ -28,6 +28,7 @@ from .search_source_connectors_routes import router as search_source_connectors_ from .search_spaces_routes import router as search_spaces_router from .slack_add_connector_route import router as slack_add_connector_router from .discord_add_connector_route import router as discord_add_connector_router +from .jira_add_connector_route import router as jira_add_connector_router router = APIRouter() @@ -48,6 +49,7 @@ router.include_router(luma_add_connector_router) router.include_router(notion_add_connector_router) router.include_router(slack_add_connector_router) router.include_router(discord_add_connector_router) +router.include_router(jira_add_connector_router) router.include_router(new_llm_config_router) # LLM configs with prompt configuration router.include_router(logs_router) router.include_router(circleback_webhook_router) # Circleback meeting webhooks diff --git a/surfsense_backend/app/routes/jira_add_connector_route.py b/surfsense_backend/app/routes/jira_add_connector_route.py new file mode 100644 index 000000000..ac415e80e --- /dev/null +++ b/surfsense_backend/app/routes/jira_add_connector_route.py @@ -0,0 +1,495 @@ +""" +Jira Connector OAuth Routes. + +Handles OAuth 2.0 authentication flow for Jira connector. +Uses Atlassian OAuth 2.0 (3LO) with accessible-resources API to discover Jira instances. +""" + +import logging +from datetime import UTC, datetime, timedelta +from uuid import UUID + +import httpx +from fastapi import APIRouter, Depends, HTTPException, Request +from fastapi.responses import RedirectResponse +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, + SearchSourceConnectorType, + User, + get_async_session, +) +from app.schemas.jira_auth_credentials import JiraAuthCredentialsBase +from app.users import current_active_user +from app.utils.oauth_security import OAuthStateManager, TokenEncryption + +logger = logging.getLogger(__name__) + +router = APIRouter() + +# Atlassian OAuth endpoints +AUTHORIZATION_URL = "https://auth.atlassian.com/authorize" +TOKEN_URL = "https://auth.atlassian.com/oauth/token" +ACCESSIBLE_RESOURCES_URL = "https://api.atlassian.com/oauth/token/accessible-resources" + +# OAuth scopes for Jira +SCOPES = [ + "read:jira-work", + "write:jira-work", + "read:jira-user", + "offline_access", # Required for refresh tokens +] + +# Initialize security utilities +_state_manager = None +_token_encryption = None + + +def get_state_manager() -> OAuthStateManager: + """Get or create OAuth state manager instance.""" + global _state_manager + if _state_manager is None: + if not config.SECRET_KEY: + raise ValueError("SECRET_KEY must be set for OAuth security") + _state_manager = OAuthStateManager(config.SECRET_KEY) + return _state_manager + + +def get_token_encryption() -> TokenEncryption: + """Get or create token encryption instance.""" + global _token_encryption + if _token_encryption is None: + if not config.SECRET_KEY: + raise ValueError("SECRET_KEY must be set for token encryption") + _token_encryption = TokenEncryption(config.SECRET_KEY) + return _token_encryption + + +@router.get("/auth/jira/connector/add") +async def connect_jira(space_id: int, user: User = Depends(current_active_user)): + """ + Initiate Jira OAuth flow. + + Args: + space_id: The search space ID + user: Current authenticated user + + Returns: + Authorization URL for redirect + """ + try: + if not space_id: + raise HTTPException(status_code=400, detail="space_id is required") + + if not config.JIRA_CLIENT_ID: + raise HTTPException(status_code=500, detail="Jira OAuth not configured.") + + if not config.SECRET_KEY: + raise HTTPException( + status_code=500, detail="SECRET_KEY not configured for OAuth security." + ) + + # Generate secure state parameter with HMAC signature + state_manager = get_state_manager() + state_encoded = state_manager.generate_secure_state(space_id, user.id) + + # Build authorization URL + from urllib.parse import urlencode + + auth_params = { + "audience": "api.atlassian.com", + "client_id": config.JIRA_CLIENT_ID, + "scope": " ".join(SCOPES), + "redirect_uri": config.JIRA_REDIRECT_URI, + "state": state_encoded, + "response_type": "code", + "prompt": "consent", # Force consent screen to get refresh token + } + + auth_url = f"{AUTHORIZATION_URL}?{urlencode(auth_params)}" + + logger.info(f"Generated Jira OAuth URL for user {user.id}, space {space_id}") + return {"auth_url": auth_url} + + except Exception as e: + logger.error(f"Failed to initiate Jira OAuth: {e!s}", exc_info=True) + raise HTTPException( + status_code=500, detail=f"Failed to initiate Jira OAuth: {e!s}" + ) from e + + +@router.get("/auth/jira/connector/callback") +async def jira_callback( + request: Request, + code: str | None = None, + error: str | None = None, + state: str | None = None, + session: AsyncSession = Depends(get_async_session), +): + """ + Handle Jira OAuth callback. + + Args: + request: FastAPI request object + code: Authorization code from Atlassian (if user granted access) + error: Error code from Atlassian (if user denied access or error occurred) + state: State parameter containing user/space info + session: Database session + + Returns: + Redirect response to frontend + """ + try: + # Handle OAuth errors (e.g., user denied access) + if error: + logger.warning(f"Jira OAuth error: {error}") + # Try to decode state to get space_id for redirect, but don't fail if it's invalid + space_id = None + if state: + try: + state_manager = get_state_manager() + data = state_manager.validate_state(state) + space_id = data.get("space_id") + except Exception: + # If state is invalid, we'll redirect without space_id + logger.warning("Failed to validate state in error handler") + + # Redirect to frontend with error parameter + if space_id: + return RedirectResponse( + url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&error=jira_oauth_denied" + ) + else: + return RedirectResponse( + url=f"{config.NEXT_FRONTEND_URL}/dashboard?error=jira_oauth_denied" + ) + + # Validate required parameters for successful flow + if not code: + raise HTTPException(status_code=400, detail="Missing authorization code") + if not state: + raise HTTPException(status_code=400, detail="Missing state parameter") + + # Validate and decode state with signature verification + state_manager = get_state_manager() + try: + data = state_manager.validate_state(state) + except HTTPException: + raise + except Exception as e: + raise HTTPException( + status_code=400, detail=f"Invalid state parameter: {e!s}" + ) from e + + user_id = UUID(data["user_id"]) + space_id = data["space_id"] + + # Validate redirect URI (security: ensure it matches configured value) + if not config.JIRA_REDIRECT_URI: + raise HTTPException( + status_code=500, detail="JIRA_REDIRECT_URI not configured" + ) + + # Exchange authorization code for access token + token_data = { + "grant_type": "authorization_code", + "client_id": config.JIRA_CLIENT_ID, + "client_secret": config.JIRA_CLIENT_SECRET, + "code": code, + "redirect_uri": config.JIRA_REDIRECT_URI, + } + + async with httpx.AsyncClient() as client: + token_response = await client.post( + TOKEN_URL, + data=token_data, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + timeout=30.0, + ) + + if token_response.status_code != 200: + error_detail = token_response.text + try: + error_json = token_response.json() + error_detail = error_json.get("error_description", error_json.get("error", error_detail)) + except Exception: + pass + raise HTTPException( + status_code=400, detail=f"Token exchange failed: {error_detail}" + ) + + token_json = token_response.json() + + # Encrypt sensitive tokens before storing + token_encryption = get_token_encryption() + access_token = token_json.get("access_token") + refresh_token = token_json.get("refresh_token") + + if not access_token: + raise HTTPException( + status_code=400, detail="No access token received from Atlassian" + ) + + # Fetch accessible resources to get Jira instance information + async with httpx.AsyncClient() as client: + resources_response = await client.get( + ACCESSIBLE_RESOURCES_URL, + headers={"Authorization": f"Bearer {access_token}"}, + timeout=30.0, + ) + + if resources_response.status_code != 200: + error_detail = resources_response.text + logger.error(f"Failed to fetch accessible resources: {error_detail}") + raise HTTPException( + status_code=400, + detail=f"Failed to fetch Jira instances: {error_detail}", + ) + + resources = resources_response.json() + + # Filter for Jira instances (resources with type "jira" or id field) + jira_instances = [ + r + for r in resources + if r.get("id") and (r.get("name") or r.get("url")) + ] + + if not jira_instances: + raise HTTPException( + status_code=400, + detail="No accessible Jira instances found. Please ensure you have access to at least one Jira instance.", + ) + + # For now, use the first Jira instance + # TODO: Support multiple instances by letting user choose during OAuth + jira_instance = jira_instances[0] + cloud_id = jira_instance["id"] + base_url = jira_instance.get("url") + + # If URL is not provided, construct it from cloud_id + if not base_url: + # Try to extract from name or construct default format + instance_name = jira_instance.get("name", "").lower().replace(" ", "") + if instance_name: + base_url = f"https://{instance_name}.atlassian.net" + else: + # Fallback: use cloud_id directly (though this may not work) + base_url = f"https://{cloud_id}.atlassian.net" + + # Calculate expiration time (UTC, tz-aware) + expires_at = None + expires_in = token_json.get("expires_in") + if expires_in: + now_utc = datetime.now(UTC) + expires_at = now_utc + timedelta(seconds=int(expires_in)) + + # Store the encrypted access token and refresh token in connector config + connector_config = { + "access_token": token_encryption.encrypt_token(access_token), + "refresh_token": token_encryption.encrypt_token(refresh_token) + if refresh_token + else None, + "token_type": token_json.get("token_type", "Bearer"), + "expires_in": expires_in, + "expires_at": expires_at.isoformat() if expires_at else None, + "scope": token_json.get("scope"), + "cloud_id": cloud_id, + "base_url": base_url.rstrip("/") if base_url else None, + # Mark that tokens are encrypted for backward compatibility + "_token_encrypted": True, + } + + # Check if connector already exists for this search space and user + existing_connector_result = await session.execute( + select(SearchSourceConnector).filter( + SearchSourceConnector.search_space_id == space_id, + SearchSourceConnector.user_id == user_id, + SearchSourceConnector.connector_type + == SearchSourceConnectorType.JIRA_CONNECTOR, + ) + ) + existing_connector = existing_connector_result.scalars().first() + + if existing_connector: + # Update existing connector + existing_connector.config = connector_config + existing_connector.name = "Jira Connector" + existing_connector.is_indexable = True + logger.info( + f"Updated existing Jira connector for user {user_id} in space {space_id}" + ) + else: + # Create new connector + new_connector = SearchSourceConnector( + name="Jira Connector", + connector_type=SearchSourceConnectorType.JIRA_CONNECTOR, + is_indexable=True, + config=connector_config, + search_space_id=space_id, + user_id=user_id, + ) + session.add(new_connector) + logger.info( + f"Created new Jira connector for user {user_id} in space {space_id}" + ) + + try: + await session.commit() + logger.info(f"Successfully saved Jira connector for user {user_id}") + + # Redirect to the frontend with success params + return RedirectResponse( + url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&success=true&connector=jira-connector" + ) + + except ValidationError as e: + await session.rollback() + raise HTTPException( + status_code=422, detail=f"Validation error: {e!s}" + ) from e + except IntegrityError as e: + await session.rollback() + raise HTTPException( + status_code=409, + detail=f"Integrity error: A connector with this type already exists. {e!s}", + ) from e + except Exception as e: + logger.error(f"Failed to create search source connector: {e!s}") + await session.rollback() + raise HTTPException( + status_code=500, + detail=f"Failed to create search source connector: {e!s}", + ) from e + + except HTTPException: + raise + except Exception as e: + logger.error(f"Failed to complete Jira OAuth: {e!s}", exc_info=True) + raise HTTPException( + status_code=500, detail=f"Failed to complete Jira OAuth: {e!s}" + ) from e + + +async def refresh_jira_token( + session: AsyncSession, connector: SearchSourceConnector +) -> SearchSourceConnector: + """ + Refresh the Jira access token for a connector. + + Args: + session: Database session + connector: Jira connector to refresh + + Returns: + Updated connector object + """ + try: + logger.info(f"Refreshing Jira token for connector {connector.id}") + + credentials = JiraAuthCredentialsBase.from_dict(connector.config) + + # Decrypt tokens if they are encrypted + token_encryption = get_token_encryption() + is_encrypted = connector.config.get("_token_encrypted", False) + + refresh_token = credentials.refresh_token + if is_encrypted and refresh_token: + try: + refresh_token = token_encryption.decrypt_token(refresh_token) + except Exception as e: + logger.error(f"Failed to decrypt refresh token: {e!s}") + raise HTTPException( + status_code=500, detail="Failed to decrypt stored refresh token" + ) from e + + if not refresh_token: + raise HTTPException( + status_code=400, + detail="No refresh token available. Please re-authenticate.", + ) + + # Prepare token refresh data + refresh_data = { + "grant_type": "refresh_token", + "client_id": config.JIRA_CLIENT_ID, + "client_secret": config.JIRA_CLIENT_SECRET, + "refresh_token": refresh_token, + } + + async with httpx.AsyncClient() as client: + token_response = await client.post( + TOKEN_URL, + data=refresh_data, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + timeout=30.0, + ) + + if token_response.status_code != 200: + error_detail = token_response.text + try: + error_json = token_response.json() + error_detail = error_json.get("error_description", error_json.get("error", error_detail)) + except Exception: + pass + raise HTTPException( + status_code=400, detail=f"Token refresh failed: {error_detail}" + ) + + token_json = token_response.json() + + # Calculate expiration time (UTC, tz-aware) + expires_at = None + expires_in = token_json.get("expires_in") + if expires_in: + now_utc = datetime.now(UTC) + expires_at = now_utc + timedelta(seconds=int(expires_in)) + + # Encrypt new tokens before storing + access_token = token_json.get("access_token") + new_refresh_token = token_json.get("refresh_token") + + if not access_token: + raise HTTPException( + status_code=400, detail="No access token received from Jira refresh" + ) + + # Update credentials object with encrypted tokens + credentials.access_token = token_encryption.encrypt_token(access_token) + if new_refresh_token: + credentials.refresh_token = token_encryption.encrypt_token( + new_refresh_token + ) + credentials.expires_in = expires_in + credentials.expires_at = expires_at + credentials.scope = token_json.get("scope") + + # Preserve cloud_id and base_url + if not credentials.cloud_id: + credentials.cloud_id = connector.config.get("cloud_id") + if not credentials.base_url: + credentials.base_url = connector.config.get("base_url") + + # Update connector config with encrypted tokens + credentials_dict = credentials.to_dict() + credentials_dict["_token_encrypted"] = True + connector.config = credentials_dict + await session.commit() + await session.refresh(connector) + + logger.info(f"Successfully refreshed Jira token for connector {connector.id}") + + return connector + except HTTPException: + raise + except Exception as e: + logger.error(f"Failed to refresh Jira token: {e!s}", exc_info=True) + raise HTTPException( + status_code=500, detail=f"Failed to refresh Jira token: {e!s}" + ) from e + diff --git a/surfsense_backend/app/schemas/jira_auth_credentials.py b/surfsense_backend/app/schemas/jira_auth_credentials.py new file mode 100644 index 000000000..23d1ffcbf --- /dev/null +++ b/surfsense_backend/app/schemas/jira_auth_credentials.py @@ -0,0 +1,73 @@ +from datetime import UTC, datetime + +from pydantic import BaseModel, field_validator + + +class JiraAuthCredentialsBase(BaseModel): + access_token: str + refresh_token: str | None = None + token_type: str = "Bearer" + expires_in: int | None = None + expires_at: datetime | None = None + scope: str | None = None + cloud_id: str | None = None + base_url: str | None = None + + @property + def is_expired(self) -> bool: + """Check if the credentials have expired.""" + if self.expires_at is None: + return False + return self.expires_at <= datetime.now(UTC) + + @property + def is_refreshable(self) -> bool: + """Check if the credentials can be refreshed.""" + return self.refresh_token is not None + + def to_dict(self) -> dict: + """Convert credentials to dictionary for storage.""" + return { + "access_token": self.access_token, + "refresh_token": self.refresh_token, + "token_type": self.token_type, + "expires_in": self.expires_in, + "expires_at": self.expires_at.isoformat() if self.expires_at else None, + "scope": self.scope, + "cloud_id": self.cloud_id, + "base_url": self.base_url, + } + + @classmethod + def from_dict(cls, data: dict) -> "JiraAuthCredentialsBase": + """Create credentials from dictionary.""" + expires_at = None + if data.get("expires_at"): + expires_at = datetime.fromisoformat(data["expires_at"]) + + return cls( + access_token=data["access_token"], + refresh_token=data.get("refresh_token"), + token_type=data.get("token_type", "Bearer"), + expires_in=data.get("expires_in"), + expires_at=expires_at, + scope=data.get("scope"), + cloud_id=data.get("cloud_id"), + base_url=data.get("base_url"), + ) + + @field_validator("expires_at", mode="before") + @classmethod + def ensure_aware_utc(cls, v): + # Strings like "2025-08-26T14:46:57.367184" + if isinstance(v, str): + # add +00:00 if missing tz info + if v.endswith("Z"): + return datetime.fromisoformat(v.replace("Z", "+00:00")) + dt = datetime.fromisoformat(v) + return dt if dt.tzinfo else dt.replace(tzinfo=UTC) + # datetime objects + if isinstance(v, datetime): + return v if v.tzinfo else v.replace(tzinfo=UTC) + return v + diff --git a/surfsense_backend/app/tasks/connector_indexers/jira_indexer.py b/surfsense_backend/app/tasks/connector_indexers/jira_indexer.py index 8c56b10ab..616927e6f 100644 --- a/surfsense_backend/app/tasks/connector_indexers/jira_indexer.py +++ b/surfsense_backend/app/tasks/connector_indexers/jira_indexer.py @@ -84,31 +84,137 @@ async def index_jira_issues( return 0, f"Connector with ID {connector_id} not found" # Get the Jira credentials from the connector config - jira_email = connector.config.get("JIRA_EMAIL") - jira_api_token = connector.config.get("JIRA_API_TOKEN") - jira_base_url = connector.config.get("JIRA_BASE_URL") + # Support both OAuth (preferred) and legacy API token authentication + config_data = connector.config.copy() + is_oauth = config_data.get("_token_encrypted", False) or config_data.get("access_token") - if not jira_email or not jira_api_token or not jira_base_url: - await task_logger.log_task_failure( + if is_oauth: + # OAuth 2.0 authentication + from app.utils.oauth_security import TokenEncryption + + if not config.SECRET_KEY: + await task_logger.log_task_failure( + log_entry, + f"SECRET_KEY not configured but tokens are marked as encrypted for connector {connector_id}", + "Missing SECRET_KEY for token decryption", + {"error_type": "MissingSecretKey"}, + ) + return 0, "SECRET_KEY not configured but tokens are marked as encrypted" + + try: + token_encryption = TokenEncryption(config.SECRET_KEY) + + # Decrypt access_token + if config_data.get("access_token"): + config_data["access_token"] = token_encryption.decrypt_token( + config_data["access_token"] + ) + logger.info( + f"Decrypted Jira access token for connector {connector_id}" + ) + + # Decrypt refresh_token if present + if config_data.get("refresh_token"): + config_data["refresh_token"] = token_encryption.decrypt_token( + config_data["refresh_token"] + ) + logger.info( + f"Decrypted Jira refresh token for connector {connector_id}" + ) + except Exception as e: + await task_logger.log_task_failure( + log_entry, + f"Failed to decrypt Jira tokens for connector {connector_id}: {e!s}", + "Token decryption failed", + {"error_type": "TokenDecryptionError"}, + ) + return 0, f"Failed to decrypt Jira tokens: {e!s}" + + try: + from app.schemas.jira_auth_credentials import JiraAuthCredentialsBase + credentials = JiraAuthCredentialsBase.from_dict(config_data) + except Exception as e: + await task_logger.log_task_failure( + log_entry, + f"Invalid Jira OAuth credentials in connector {connector_id}", + str(e), + {"error_type": "InvalidCredentials"}, + ) + return 0, f"Invalid Jira OAuth credentials: {e!s}" + + # Check if credentials are expired and refresh if needed + if credentials.is_expired: + await task_logger.log_task_progress( + log_entry, + f"Jira credentials expired for connector {connector_id}, refreshing token", + {"stage": "token_refresh"}, + ) + + from app.routes.jira_add_connector_route import refresh_jira_token + + try: + connector = await refresh_jira_token(session, connector) + # Re-fetch credentials after refresh + config_data = connector.config.copy() + if config_data.get("access_token"): + config_data["access_token"] = token_encryption.decrypt_token( + config_data["access_token"] + ) + credentials = JiraAuthCredentialsBase.from_dict(config_data) + except Exception as e: + await task_logger.log_task_failure( + log_entry, + f"Failed to refresh Jira token for connector {connector_id}: {e!s}", + "Token refresh failed", + {"error_type": "TokenRefreshError"}, + ) + return 0, f"Failed to refresh Jira token: {e!s}" + + # Initialize Jira client with OAuth credentials + await task_logger.log_task_progress( log_entry, - f"Jira credentials not found in connector config for connector {connector_id}", - "Missing Jira credentials", - {"error_type": "MissingCredentials"}, + f"Initializing Jira client with OAuth for connector {connector_id}", + {"stage": "client_initialization"}, ) - return 0, "Jira credentials not found in connector config" - # Initialize Jira client - await task_logger.log_task_progress( - log_entry, - f"Initializing Jira client for connector {connector_id}", - {"stage": "client_initialization"}, - ) + jira_client = JiraConnector( + base_url=credentials.base_url, + access_token=credentials.access_token, + cloud_id=credentials.cloud_id, + ) + else: + # Legacy API token authentication + jira_email = config_data.get("JIRA_EMAIL") + jira_api_token = config_data.get("JIRA_API_TOKEN") + jira_base_url = config_data.get("JIRA_BASE_URL") - jira_client = JiraConnector( - base_url=jira_base_url, email=jira_email, api_token=jira_api_token - ) + if not jira_email or not jira_api_token or not jira_base_url: + await task_logger.log_task_failure( + log_entry, + f"Jira credentials not found in connector config for connector {connector_id}", + "Missing Jira credentials", + {"error_type": "MissingCredentials"}, + ) + return 0, "Jira credentials not found in connector config" + + # Initialize Jira client with legacy credentials + await task_logger.log_task_progress( + log_entry, + f"Initializing Jira client with API token for connector {connector_id}", + {"stage": "client_initialization"}, + ) + + jira_client = JiraConnector( + base_url=jira_base_url, email=jira_email, api_token=jira_api_token + ) # Calculate date range + # Handle "undefined" strings from frontend + if start_date == "undefined" or start_date == "": + start_date = None + if end_date == "undefined" or end_date == "": + end_date = None + start_date_str, end_date_str = calculate_date_range( connector, start_date, end_date, default_days_back=365 ) diff --git a/surfsense_backend/app/utils/validators.py b/surfsense_backend/app/utils/validators.py index f1620c0e5..d1f416339 100644 --- a/surfsense_backend/app/utils/validators.py +++ b/surfsense_backend/app/utils/validators.py @@ -538,13 +538,13 @@ def validate_connector_config( }, }, # "DISCORD_CONNECTOR": {"required": ["DISCORD_BOT_TOKEN"], "validators": {}}, - "JIRA_CONNECTOR": { - "required": ["JIRA_EMAIL", "JIRA_API_TOKEN", "JIRA_BASE_URL"], - "validators": { - "JIRA_EMAIL": lambda: validate_email_field("JIRA_EMAIL", "JIRA"), - "JIRA_BASE_URL": lambda: validate_url_field("JIRA_BASE_URL", "JIRA"), - }, - }, + # "JIRA_CONNECTOR": { + # "required": ["JIRA_EMAIL", "JIRA_API_TOKEN", "JIRA_BASE_URL"], + # "validators": { + # "JIRA_EMAIL": lambda: validate_email_field("JIRA_EMAIL", "JIRA"), + # "JIRA_BASE_URL": lambda: validate_url_field("JIRA_BASE_URL", "JIRA"), + # }, + # }, "CONFLUENCE_CONNECTOR": { "required": [ "CONFLUENCE_BASE_URL", diff --git a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/jira-connect-form.tsx b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/jira-connect-form.tsx deleted file mode 100644 index 0499554b4..000000000 --- a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/jira-connect-form.tsx +++ /dev/null @@ -1,450 +0,0 @@ -"use client"; - -import { zodResolver } from "@hookform/resolvers/zod"; -import { Info } from "lucide-react"; -import type { FC } from "react"; -import { useRef, useState } from "react"; -import { useForm } from "react-hook-form"; -import * as z from "zod"; -import { - Accordion, - AccordionContent, - AccordionItem, - AccordionTrigger, -} from "@/components/ui/accordion"; -import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; -import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { Switch } from "@/components/ui/switch"; -import { EnumConnectorName } from "@/contracts/enums/connector"; -import { DateRangeSelector } from "../../components/date-range-selector"; -import { getConnectorBenefits } from "../connector-benefits"; -import type { ConnectFormProps } from "../index"; - -const jiraConnectorFormSchema = z.object({ - name: z.string().min(3, { - message: "Connector name must be at least 3 characters.", - }), - base_url: z.string().url({ message: "Please enter a valid Jira base URL." }), - email: z.string().email({ message: "Please enter a valid email address." }), - api_token: z.string().min(10, { - message: "Jira API Token is required and must be valid.", - }), -}); - -type JiraConnectorFormValues = z.infer; - -export const JiraConnectForm: FC = ({ onSubmit, isSubmitting }) => { - const isSubmittingRef = useRef(false); - const [startDate, setStartDate] = useState(undefined); - const [endDate, setEndDate] = useState(undefined); - const [periodicEnabled, setPeriodicEnabled] = useState(false); - const [frequencyMinutes, setFrequencyMinutes] = useState("1440"); - const form = useForm({ - resolver: zodResolver(jiraConnectorFormSchema), - defaultValues: { - name: "Jira Connector", - base_url: "", - email: "", - api_token: "", - }, - }); - - const handleSubmit = async (values: JiraConnectorFormValues) => { - // Prevent multiple submissions - if (isSubmittingRef.current || isSubmitting) { - return; - } - - isSubmittingRef.current = true; - try { - await onSubmit({ - name: values.name, - connector_type: EnumConnectorName.JIRA_CONNECTOR, - config: { - JIRA_BASE_URL: values.base_url, - JIRA_EMAIL: values.email, - JIRA_API_TOKEN: values.api_token, - }, - is_indexable: true, - last_indexed_at: null, - periodic_indexing_enabled: periodicEnabled, - indexing_frequency_minutes: periodicEnabled ? parseInt(frequencyMinutes, 10) : null, - next_scheduled_at: null, - startDate, - endDate, - periodicEnabled, - frequencyMinutes, - }); - } finally { - isSubmittingRef.current = false; - } - }; - - return ( -
- - -
- API Token Required - - You'll need a Jira API Token to use this connector. You can create one from{" "} - - Atlassian Account Settings - - -
-
- -
-
- - ( - - Connector Name - - - - - A friendly name to identify this connector. - - - - )} - /> - - ( - - Jira Base URL - - - - - The base URL of your Jira instance (e.g., https://your-domain.atlassian.net). - - - - )} - /> - - ( - - Email Address - - - - - The email address associated with your Atlassian account. - - - - )} - /> - - ( - - API Token - - - - - Your Jira API Token will be encrypted and stored securely. - - - - )} - /> - - {/* Indexing Configuration */} -
-

Indexing Configuration

- - {/* Date Range Selector */} - - - {/* Periodic Sync Config */} -
-
-
-

Enable Periodic Sync

-

- Automatically re-index at regular intervals -

-
- -
- - {periodicEnabled && ( -
-
- - -
-
- )} -
-
- - -
- - {/* What you get section */} - {getConnectorBenefits(EnumConnectorName.JIRA_CONNECTOR) && ( -
-

What you get with Jira integration:

-
    - {getConnectorBenefits(EnumConnectorName.JIRA_CONNECTOR)?.map((benefit) => ( -
  • {benefit}
  • - ))} -
-
- )} - - {/* Documentation Section */} - - - - Documentation - - -
-

How it works

-

- The Jira connector uses the Jira REST API with Basic Authentication to fetch all - issues and comments that your account has access to within your Jira instance. -

-
    -
  • - For follow up indexing runs, the connector retrieves issues and comments that have - been updated since the last indexing attempt. -
  • -
  • - Indexing is configured to run periodically, so updates should appear in your - search results within minutes. -
  • -
-
- -
-
-

Authorization

- - - - Read-Only Access is Sufficient - - - You only need read access for this connector to work. The API Token will only be - used to read your Jira data. - - - -
-
-

- Step 1: Create an API Token -

-
    -
  1. Log in to your Atlassian account
  2. -
  3. - Navigate to{" "} - - https://id.atlassian.com/manage-profile/security/api-tokens - {" "} - in your browser. -
  4. -
  5. - Click Create API token -
  6. -
  7. Enter a label for your token (like "SurfSense Connector")
  8. -
  9. - Click Create -
  10. -
  11. Copy the generated token as it will only be shown once
  12. -
-
- -
-

- Step 2: Grant necessary access -

-

- The API Token will have access to all projects and issues that your user - account can see. Make sure your account has appropriate permissions for the - projects you want to index. -

- - - Data Privacy - - Only issues, comments, and basic metadata will be indexed. Jira attachments - and linked files are not indexed by this connector. - - -
-
-
-
- -
-
-

Indexing

-
    -
  1. - Navigate to the Connector Dashboard and select the Jira{" "} - Connector. -
  2. -
  3. - Enter your Jira Instance URL (e.g., - https://yourcompany.atlassian.net) -
  4. -
  5. - Enter your Email Address associated with your Atlassian account -
  6. -
  7. - Place your API Token in the form field. -
  8. -
  9. - Click Connect to establish the connection. -
  10. -
  11. Once connected, your Jira issues will be indexed automatically.
  12. -
- - - - What Gets Indexed - -

The Jira connector indexes the following data:

-
    -
  • Issue keys and summaries (e.g., PROJ-123)
  • -
  • Issue descriptions
  • -
  • Issue comments and discussion threads
  • -
  • Issue status, priority, and type information
  • -
  • Assignee and reporter information
  • -
  • Project information
  • -
-
-
-
-
-
-
-
-
- ); -}; diff --git a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/index.tsx b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/index.tsx index 81e5ee03f..cda17ddfc 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/index.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/index.tsx @@ -6,7 +6,6 @@ import { ClickUpConnectForm } from "./components/clickup-connect-form"; import { ConfluenceConnectForm } from "./components/confluence-connect-form"; import { ElasticsearchConnectForm } from "./components/elasticsearch-connect-form"; import { GithubConnectForm } from "./components/github-connect-form"; -import { JiraConnectForm } from "./components/jira-connect-form"; import { LinkupApiConnectForm } from "./components/linkup-api-connect-form"; import { LumaConnectForm } from "./components/luma-connect-form"; import { SearxngConnectForm } from "./components/searxng-connect-form"; @@ -55,8 +54,6 @@ export function getConnectFormComponent(connectorType: string): ConnectFormCompo return BookStackConnectForm; case "GITHUB_CONNECTOR": return GithubConnectForm; - case "JIRA_CONNECTOR": - return JiraConnectForm; case "CLICKUP_CONNECTOR": return ClickUpConnectForm; case "LUMA_CONNECTOR": diff --git a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/jira-config.tsx b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/jira-config.tsx index 3ef16bdb4..158dfdf13 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/jira-config.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/jira-config.tsx @@ -1,6 +1,6 @@ "use client"; -import { KeyRound } from "lucide-react"; +import { Info, KeyRound } from "lucide-react"; import type { FC } from "react"; import { useEffect, useState } from "react"; import { Input } from "@/components/ui/input"; @@ -12,6 +12,9 @@ export interface JiraConfigProps extends ConnectorConfigProps { } export const JiraConfig: FC = ({ connector, onConfigChange, onNameChange }) => { + // Check if this is an OAuth connector (has access_token or _token_encrypted flag) + const isOAuth = !!(connector.config?.access_token || connector.config?._token_encrypted); + const [baseUrl, setBaseUrl] = useState((connector.config?.JIRA_BASE_URL as string) || ""); const [email, setEmail] = useState((connector.config?.JIRA_EMAIL as string) || ""); const [apiToken, setApiToken] = useState( @@ -19,16 +22,18 @@ export const JiraConfig: FC = ({ connector, onConfigChange, onN ); const [name, setName] = useState(connector.name || ""); - // Update values when connector changes + // Update values when connector changes (only for legacy connectors) useEffect(() => { - const url = (connector.config?.JIRA_BASE_URL as string) || ""; - const emailVal = (connector.config?.JIRA_EMAIL as string) || ""; - const token = (connector.config?.JIRA_API_TOKEN as string) || ""; - setBaseUrl(url); - setEmail(emailVal); - setApiToken(token); + if (!isOAuth) { + const url = (connector.config?.JIRA_BASE_URL as string) || ""; + const emailVal = (connector.config?.JIRA_EMAIL as string) || ""; + const token = (connector.config?.JIRA_API_TOKEN as string) || ""; + setBaseUrl(url); + setEmail(emailVal); + setApiToken(token); + } setName(connector.name || ""); - }, [connector.config, connector.name]); + }, [connector.config, connector.name, isOAuth]); const handleBaseUrlChange = (value: string) => { setBaseUrl(value); @@ -67,6 +72,34 @@ export const JiraConfig: FC = ({ connector, onConfigChange, onN } }; + // For OAuth connectors, show simple info message + if (isOAuth) { + const baseUrl = (connector.config?.base_url as string) || "Unknown"; + return ( +
+ {/* OAuth Info */} +
+
+ +
+
+

Connected via OAuth

+

+ This connector is authenticated using OAuth 2.0. Your Jira instance is: +

+

+ {baseUrl} +

+

+ To update your connection, disconnect and reconnect through the OAuth flow. +

+
+
+
+ ); + } + + // For legacy API token connectors, show the form return (
{/* Connector Name */} diff --git a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-connect-view.tsx b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-connect-view.tsx index 3ba03f956..7b0c3e82f 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-connect-view.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-connect-view.tsx @@ -55,7 +55,6 @@ export const ConnectorConnectView: FC = ({ CONFLUENCE_CONNECTOR: "confluence-connect-form", BOOKSTACK_CONNECTOR: "bookstack-connect-form", GITHUB_CONNECTOR: "github-connect-form", - JIRA_CONNECTOR: "jira-connect-form", CLICKUP_CONNECTOR: "clickup-connect-form", LUMA_CONNECTOR: "luma-connect-form", CIRCLEBACK_CONNECTOR: "circleback-connect-form", diff --git a/surfsense_web/components/assistant-ui/connector-popup/constants/connector-constants.ts b/surfsense_web/components/assistant-ui/connector-popup/constants/connector-constants.ts index 9822ff6e6..0e942dd1e 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/constants/connector-constants.ts +++ b/surfsense_web/components/assistant-ui/connector-popup/constants/connector-constants.ts @@ -58,6 +58,13 @@ export const OAUTH_CONNECTORS = [ connectorType: EnumConnectorName.DISCORD_CONNECTOR, authEndpoint: "/api/v1/auth/discord/connector/add/", }, + { + id: "jira-connector", + title: "Jira", + description: "Search Jira issues", + connectorType: EnumConnectorName.JIRA_CONNECTOR, + authEndpoint: "/api/v1/auth/jira/connector/add/", + }, ] as const; // Content Sources (tools that extract and import content from external sources) @@ -96,12 +103,6 @@ export const OTHER_CONNECTORS = [ description: "Search repositories", connectorType: EnumConnectorName.GITHUB_CONNECTOR, }, - { - id: "jira-connector", - title: "Jira", - description: "Search Jira issues", - connectorType: EnumConnectorName.JIRA_CONNECTOR, - }, { id: "clickup-connector", title: "ClickUp", diff --git a/surfsense_web/hooks/use-connector-edit-page.ts b/surfsense_web/hooks/use-connector-edit-page.ts index 3beb80247..ba4ba6b58 100644 --- a/surfsense_web/hooks/use-connector-edit-page.ts +++ b/surfsense_web/hooks/use-connector-edit-page.ts @@ -447,6 +447,16 @@ export function useConnectorEditPage(connectorId: number, searchSpaceId: string) } break; case "JIRA_CONNECTOR": + // Check if this is an OAuth connector (has access_token or _token_encrypted flag) + const isJiraOAuth = !!(originalConfig.access_token || originalConfig._token_encrypted); + + if (isJiraOAuth) { + // OAuth connectors don't allow editing credentials through the form + // Only allow name changes, which are handled separately + break; + } + + // Legacy API token connector - allow editing credentials if ( formData.JIRA_BASE_URL !== originalConfig.JIRA_BASE_URL || formData.JIRA_EMAIL !== originalConfig.JIRA_EMAIL || From f236110a08176986667c8dd1580a9301604c489e Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Tue, 6 Jan 2026 00:09:08 +0530 Subject: [PATCH 19/75] Revert "feat: implement Jira OAuth integration and connector routes" This reverts commit bfed9a31f822929cc2b59b6c78a6ebe58a6c97d8. --- surfsense_backend/.env.example | 5 - surfsense_backend/app/config/__init__.py | 5 - .../app/connectors/jira_connector.py | 103 +--- surfsense_backend/app/routes/__init__.py | 2 - .../app/routes/jira_add_connector_route.py | 495 ------------------ .../app/schemas/jira_auth_credentials.py | 73 --- .../tasks/connector_indexers/jira_indexer.py | 142 +---- surfsense_backend/app/utils/validators.py | 14 +- .../components/jira-connect-form.tsx | 450 ++++++++++++++++ .../connector-popup/connect-forms/index.tsx | 3 + .../components/jira-config.tsx | 51 +- .../views/connector-connect-view.tsx | 1 + .../constants/connector-constants.ts | 13 +- .../hooks/use-connector-edit-page.ts | 10 - 14 files changed, 522 insertions(+), 845 deletions(-) delete mode 100644 surfsense_backend/app/routes/jira_add_connector_route.py delete mode 100644 surfsense_backend/app/schemas/jira_auth_credentials.py create mode 100644 surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/jira-connect-form.tsx diff --git a/surfsense_backend/.env.example b/surfsense_backend/.env.example index a2f662c23..d2c667178 100644 --- a/surfsense_backend/.env.example +++ b/surfsense_backend/.env.example @@ -50,11 +50,6 @@ DISCORD_CLIENT_SECRET=your_discord_client_secret_here DISCORD_REDIRECT_URI=http://localhost:8000/api/v1/auth/discord/connector/callback DISCORD_BOT_TOKEN=your_bot_token_from_developer_portal -# Jira OAuth Configuration -JIRA_CLIENT_ID=our_jira_client_id -JIRA_CLIENT_SECRET=your_jira_client_secret -JIRA_REDIRECT_URI=http://localhost:8000/api/v1/auth/jira/connector/callback - # OAuth for Linear Connector LINEAR_CLIENT_ID=your_linear_client_id LINEAR_CLIENT_SECRET=your_linear_client_secret diff --git a/surfsense_backend/app/config/__init__.py b/surfsense_backend/app/config/__init__.py index 56641215d..f65a94cc0 100644 --- a/surfsense_backend/app/config/__init__.py +++ b/surfsense_backend/app/config/__init__.py @@ -111,11 +111,6 @@ class Config: DISCORD_REDIRECT_URI = os.getenv("DISCORD_REDIRECT_URI") DISCORD_BOT_TOKEN = os.getenv("DISCORD_BOT_TOKEN") - # Jira OAuth - JIRA_CLIENT_ID = os.getenv("JIRA_CLIENT_ID") - JIRA_CLIENT_SECRET = os.getenv("JIRA_CLIENT_SECRET") - JIRA_REDIRECT_URI = os.getenv("JIRA_REDIRECT_URI") - # LLM instances are now managed per-user through the LLMConfig system # Legacy environment variables removed in favor of user-specific configurations diff --git a/surfsense_backend/app/connectors/jira_connector.py b/surfsense_backend/app/connectors/jira_connector.py index 8e9badf0b..e73198e79 100644 --- a/surfsense_backend/app/connectors/jira_connector.py +++ b/surfsense_backend/app/connectors/jira_connector.py @@ -3,7 +3,6 @@ Jira Connector Module A module for retrieving data from Jira. Allows fetching issue lists and their comments, projects and more. -Supports both OAuth 2.0 (preferred) and legacy API token authentication. """ import base64 @@ -19,8 +18,6 @@ class JiraConnector: def __init__( self, base_url: str | None = None, - access_token: str | None = None, - cloud_id: str | None = None, email: str | None = None, api_token: str | None = None, ): @@ -28,39 +25,18 @@ class JiraConnector: Initialize the JiraConnector class. Args: - base_url: Jira instance base URL (e.g., 'https://yourcompany.atlassian.net') - access_token: OAuth 2.0 access token (preferred method) - cloud_id: Atlassian cloud ID (used with OAuth for API URL construction) - email: Jira account email address (legacy method, used with api_token) - api_token: Jira API token (legacy method, used with email) + base_url: Jira instance base URL (e.g., 'https://yourcompany.atlassian.net') (optional) + email: Jira account email address (optional) + api_token: Jira API token (optional) """ self.base_url = base_url.rstrip("/") if base_url else None - self.access_token = access_token - self.cloud_id = cloud_id self.email = email self.api_token = api_token self.api_version = "3" # Jira Cloud API version - self._use_oauth = access_token is not None - - def set_oauth_credentials( - self, base_url: str, access_token: str, cloud_id: str | None = None - ) -> None: - """ - Set OAuth 2.0 credentials (preferred method). - - Args: - base_url: Jira instance base URL - access_token: OAuth 2.0 access token - cloud_id: Atlassian cloud ID (optional, used for API URL construction) - """ - self.base_url = base_url.rstrip("/") - self.access_token = access_token - self.cloud_id = cloud_id - self._use_oauth = True def set_credentials(self, base_url: str, email: str, api_token: str) -> None: """ - Set the Jira credentials (legacy method using API token). + Set the Jira credentials. Args: base_url: Jira instance base URL @@ -70,69 +46,50 @@ class JiraConnector: self.base_url = base_url.rstrip("/") self.email = email self.api_token = api_token - self._use_oauth = False def set_email(self, email: str) -> None: """ - Set the Jira account email (legacy method). + Set the Jira account email. Args: email: Jira account email address """ self.email = email - self._use_oauth = False def set_api_token(self, api_token: str) -> None: """ - Set the Jira API token (legacy method). + Set the Jira API token. Args: api_token: Jira API token """ self.api_token = api_token - self._use_oauth = False def get_headers(self) -> dict[str, str]: """ - Get headers for Jira API requests. - - Uses OAuth Bearer token if available, otherwise falls back to Basic Auth. + Get headers for Jira API requests using Basic Authentication. Returns: Dictionary of headers Raises: - ValueError: If credentials have not been set + ValueError: If email, api_token, or base_url have not been set """ - if self._use_oauth: - # OAuth 2.0 authentication - if not self.base_url or not self.access_token: - raise ValueError( - "Jira OAuth credentials not initialized. Call set_oauth_credentials() first." - ) + if not all([self.base_url, self.email, self.api_token]): + raise ValueError( + "Jira credentials not initialized. Call set_credentials() first." + ) - return { - "Content-Type": "application/json", - "Authorization": f"Bearer {self.access_token}", - "Accept": "application/json", - } - else: - # Legacy Basic Auth - if not all([self.base_url, self.email, self.api_token]): - raise ValueError( - "Jira credentials not initialized. Call set_credentials() first." - ) + # Create Basic Auth header using email:api_token + auth_str = f"{self.email}:{self.api_token}" + auth_bytes = auth_str.encode("utf-8") + auth_header = "Basic " + base64.b64encode(auth_bytes).decode("ascii") - # Create Basic Auth header using email:api_token - auth_str = f"{self.email}:{self.api_token}" - auth_bytes = auth_str.encode("utf-8") - auth_header = "Basic " + base64.b64encode(auth_bytes).decode("ascii") - - return { - "Content-Type": "application/json", - "Authorization": auth_header, - "Accept": "application/json", - } + return { + "Content-Type": "application/json", + "Authorization": auth_header, + "Accept": "application/json", + } def make_api_request( self, @@ -147,25 +104,21 @@ class JiraConnector: Args: endpoint: API endpoint (without base URL) params: Query parameters for the request (optional) - method: HTTP method (GET or POST) - json_payload: JSON payload for POST requests (optional) Returns: Response data from the API Raises: - ValueError: If credentials have not been set + ValueError: If email, api_token, or base_url have not been set Exception: If the API request fails """ - headers = self.get_headers() + if not all([self.base_url, self.email, self.api_token]): + raise ValueError( + "Jira credentials not initialized. Call set_credentials() first." + ) - # Construct API URL based on authentication method - if self._use_oauth and self.cloud_id: - # Use Atlassian API gateway with cloud_id for OAuth - url = f"https://api.atlassian.com/ex/jira/{self.cloud_id}/rest/api/{self.api_version}/{endpoint}" - else: - # Use direct base URL (works for both OAuth and legacy) - url = f"{self.base_url}/rest/api/{self.api_version}/{endpoint}" + url = f"{self.base_url}/rest/api/{self.api_version}/{endpoint}" + headers = self.get_headers() if method.upper() == "POST": response = requests.post( diff --git a/surfsense_backend/app/routes/__init__.py b/surfsense_backend/app/routes/__init__.py index 16cacfeb8..b35d743e0 100644 --- a/surfsense_backend/app/routes/__init__.py +++ b/surfsense_backend/app/routes/__init__.py @@ -28,7 +28,6 @@ from .search_source_connectors_routes import router as search_source_connectors_ from .search_spaces_routes import router as search_spaces_router from .slack_add_connector_route import router as slack_add_connector_router from .discord_add_connector_route import router as discord_add_connector_router -from .jira_add_connector_route import router as jira_add_connector_router router = APIRouter() @@ -49,7 +48,6 @@ router.include_router(luma_add_connector_router) router.include_router(notion_add_connector_router) router.include_router(slack_add_connector_router) router.include_router(discord_add_connector_router) -router.include_router(jira_add_connector_router) router.include_router(new_llm_config_router) # LLM configs with prompt configuration router.include_router(logs_router) router.include_router(circleback_webhook_router) # Circleback meeting webhooks diff --git a/surfsense_backend/app/routes/jira_add_connector_route.py b/surfsense_backend/app/routes/jira_add_connector_route.py deleted file mode 100644 index ac415e80e..000000000 --- a/surfsense_backend/app/routes/jira_add_connector_route.py +++ /dev/null @@ -1,495 +0,0 @@ -""" -Jira Connector OAuth Routes. - -Handles OAuth 2.0 authentication flow for Jira connector. -Uses Atlassian OAuth 2.0 (3LO) with accessible-resources API to discover Jira instances. -""" - -import logging -from datetime import UTC, datetime, timedelta -from uuid import UUID - -import httpx -from fastapi import APIRouter, Depends, HTTPException, Request -from fastapi.responses import RedirectResponse -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, - SearchSourceConnectorType, - User, - get_async_session, -) -from app.schemas.jira_auth_credentials import JiraAuthCredentialsBase -from app.users import current_active_user -from app.utils.oauth_security import OAuthStateManager, TokenEncryption - -logger = logging.getLogger(__name__) - -router = APIRouter() - -# Atlassian OAuth endpoints -AUTHORIZATION_URL = "https://auth.atlassian.com/authorize" -TOKEN_URL = "https://auth.atlassian.com/oauth/token" -ACCESSIBLE_RESOURCES_URL = "https://api.atlassian.com/oauth/token/accessible-resources" - -# OAuth scopes for Jira -SCOPES = [ - "read:jira-work", - "write:jira-work", - "read:jira-user", - "offline_access", # Required for refresh tokens -] - -# Initialize security utilities -_state_manager = None -_token_encryption = None - - -def get_state_manager() -> OAuthStateManager: - """Get or create OAuth state manager instance.""" - global _state_manager - if _state_manager is None: - if not config.SECRET_KEY: - raise ValueError("SECRET_KEY must be set for OAuth security") - _state_manager = OAuthStateManager(config.SECRET_KEY) - return _state_manager - - -def get_token_encryption() -> TokenEncryption: - """Get or create token encryption instance.""" - global _token_encryption - if _token_encryption is None: - if not config.SECRET_KEY: - raise ValueError("SECRET_KEY must be set for token encryption") - _token_encryption = TokenEncryption(config.SECRET_KEY) - return _token_encryption - - -@router.get("/auth/jira/connector/add") -async def connect_jira(space_id: int, user: User = Depends(current_active_user)): - """ - Initiate Jira OAuth flow. - - Args: - space_id: The search space ID - user: Current authenticated user - - Returns: - Authorization URL for redirect - """ - try: - if not space_id: - raise HTTPException(status_code=400, detail="space_id is required") - - if not config.JIRA_CLIENT_ID: - raise HTTPException(status_code=500, detail="Jira OAuth not configured.") - - if not config.SECRET_KEY: - raise HTTPException( - status_code=500, detail="SECRET_KEY not configured for OAuth security." - ) - - # Generate secure state parameter with HMAC signature - state_manager = get_state_manager() - state_encoded = state_manager.generate_secure_state(space_id, user.id) - - # Build authorization URL - from urllib.parse import urlencode - - auth_params = { - "audience": "api.atlassian.com", - "client_id": config.JIRA_CLIENT_ID, - "scope": " ".join(SCOPES), - "redirect_uri": config.JIRA_REDIRECT_URI, - "state": state_encoded, - "response_type": "code", - "prompt": "consent", # Force consent screen to get refresh token - } - - auth_url = f"{AUTHORIZATION_URL}?{urlencode(auth_params)}" - - logger.info(f"Generated Jira OAuth URL for user {user.id}, space {space_id}") - return {"auth_url": auth_url} - - except Exception as e: - logger.error(f"Failed to initiate Jira OAuth: {e!s}", exc_info=True) - raise HTTPException( - status_code=500, detail=f"Failed to initiate Jira OAuth: {e!s}" - ) from e - - -@router.get("/auth/jira/connector/callback") -async def jira_callback( - request: Request, - code: str | None = None, - error: str | None = None, - state: str | None = None, - session: AsyncSession = Depends(get_async_session), -): - """ - Handle Jira OAuth callback. - - Args: - request: FastAPI request object - code: Authorization code from Atlassian (if user granted access) - error: Error code from Atlassian (if user denied access or error occurred) - state: State parameter containing user/space info - session: Database session - - Returns: - Redirect response to frontend - """ - try: - # Handle OAuth errors (e.g., user denied access) - if error: - logger.warning(f"Jira OAuth error: {error}") - # Try to decode state to get space_id for redirect, but don't fail if it's invalid - space_id = None - if state: - try: - state_manager = get_state_manager() - data = state_manager.validate_state(state) - space_id = data.get("space_id") - except Exception: - # If state is invalid, we'll redirect without space_id - logger.warning("Failed to validate state in error handler") - - # Redirect to frontend with error parameter - if space_id: - return RedirectResponse( - url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&error=jira_oauth_denied" - ) - else: - return RedirectResponse( - url=f"{config.NEXT_FRONTEND_URL}/dashboard?error=jira_oauth_denied" - ) - - # Validate required parameters for successful flow - if not code: - raise HTTPException(status_code=400, detail="Missing authorization code") - if not state: - raise HTTPException(status_code=400, detail="Missing state parameter") - - # Validate and decode state with signature verification - state_manager = get_state_manager() - try: - data = state_manager.validate_state(state) - except HTTPException: - raise - except Exception as e: - raise HTTPException( - status_code=400, detail=f"Invalid state parameter: {e!s}" - ) from e - - user_id = UUID(data["user_id"]) - space_id = data["space_id"] - - # Validate redirect URI (security: ensure it matches configured value) - if not config.JIRA_REDIRECT_URI: - raise HTTPException( - status_code=500, detail="JIRA_REDIRECT_URI not configured" - ) - - # Exchange authorization code for access token - token_data = { - "grant_type": "authorization_code", - "client_id": config.JIRA_CLIENT_ID, - "client_secret": config.JIRA_CLIENT_SECRET, - "code": code, - "redirect_uri": config.JIRA_REDIRECT_URI, - } - - async with httpx.AsyncClient() as client: - token_response = await client.post( - TOKEN_URL, - data=token_data, - headers={"Content-Type": "application/x-www-form-urlencoded"}, - timeout=30.0, - ) - - if token_response.status_code != 200: - error_detail = token_response.text - try: - error_json = token_response.json() - error_detail = error_json.get("error_description", error_json.get("error", error_detail)) - except Exception: - pass - raise HTTPException( - status_code=400, detail=f"Token exchange failed: {error_detail}" - ) - - token_json = token_response.json() - - # Encrypt sensitive tokens before storing - token_encryption = get_token_encryption() - access_token = token_json.get("access_token") - refresh_token = token_json.get("refresh_token") - - if not access_token: - raise HTTPException( - status_code=400, detail="No access token received from Atlassian" - ) - - # Fetch accessible resources to get Jira instance information - async with httpx.AsyncClient() as client: - resources_response = await client.get( - ACCESSIBLE_RESOURCES_URL, - headers={"Authorization": f"Bearer {access_token}"}, - timeout=30.0, - ) - - if resources_response.status_code != 200: - error_detail = resources_response.text - logger.error(f"Failed to fetch accessible resources: {error_detail}") - raise HTTPException( - status_code=400, - detail=f"Failed to fetch Jira instances: {error_detail}", - ) - - resources = resources_response.json() - - # Filter for Jira instances (resources with type "jira" or id field) - jira_instances = [ - r - for r in resources - if r.get("id") and (r.get("name") or r.get("url")) - ] - - if not jira_instances: - raise HTTPException( - status_code=400, - detail="No accessible Jira instances found. Please ensure you have access to at least one Jira instance.", - ) - - # For now, use the first Jira instance - # TODO: Support multiple instances by letting user choose during OAuth - jira_instance = jira_instances[0] - cloud_id = jira_instance["id"] - base_url = jira_instance.get("url") - - # If URL is not provided, construct it from cloud_id - if not base_url: - # Try to extract from name or construct default format - instance_name = jira_instance.get("name", "").lower().replace(" ", "") - if instance_name: - base_url = f"https://{instance_name}.atlassian.net" - else: - # Fallback: use cloud_id directly (though this may not work) - base_url = f"https://{cloud_id}.atlassian.net" - - # Calculate expiration time (UTC, tz-aware) - expires_at = None - expires_in = token_json.get("expires_in") - if expires_in: - now_utc = datetime.now(UTC) - expires_at = now_utc + timedelta(seconds=int(expires_in)) - - # Store the encrypted access token and refresh token in connector config - connector_config = { - "access_token": token_encryption.encrypt_token(access_token), - "refresh_token": token_encryption.encrypt_token(refresh_token) - if refresh_token - else None, - "token_type": token_json.get("token_type", "Bearer"), - "expires_in": expires_in, - "expires_at": expires_at.isoformat() if expires_at else None, - "scope": token_json.get("scope"), - "cloud_id": cloud_id, - "base_url": base_url.rstrip("/") if base_url else None, - # Mark that tokens are encrypted for backward compatibility - "_token_encrypted": True, - } - - # Check if connector already exists for this search space and user - existing_connector_result = await session.execute( - select(SearchSourceConnector).filter( - SearchSourceConnector.search_space_id == space_id, - SearchSourceConnector.user_id == user_id, - SearchSourceConnector.connector_type - == SearchSourceConnectorType.JIRA_CONNECTOR, - ) - ) - existing_connector = existing_connector_result.scalars().first() - - if existing_connector: - # Update existing connector - existing_connector.config = connector_config - existing_connector.name = "Jira Connector" - existing_connector.is_indexable = True - logger.info( - f"Updated existing Jira connector for user {user_id} in space {space_id}" - ) - else: - # Create new connector - new_connector = SearchSourceConnector( - name="Jira Connector", - connector_type=SearchSourceConnectorType.JIRA_CONNECTOR, - is_indexable=True, - config=connector_config, - search_space_id=space_id, - user_id=user_id, - ) - session.add(new_connector) - logger.info( - f"Created new Jira connector for user {user_id} in space {space_id}" - ) - - try: - await session.commit() - logger.info(f"Successfully saved Jira connector for user {user_id}") - - # Redirect to the frontend with success params - return RedirectResponse( - url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&success=true&connector=jira-connector" - ) - - except ValidationError as e: - await session.rollback() - raise HTTPException( - status_code=422, detail=f"Validation error: {e!s}" - ) from e - except IntegrityError as e: - await session.rollback() - raise HTTPException( - status_code=409, - detail=f"Integrity error: A connector with this type already exists. {e!s}", - ) from e - except Exception as e: - logger.error(f"Failed to create search source connector: {e!s}") - await session.rollback() - raise HTTPException( - status_code=500, - detail=f"Failed to create search source connector: {e!s}", - ) from e - - except HTTPException: - raise - except Exception as e: - logger.error(f"Failed to complete Jira OAuth: {e!s}", exc_info=True) - raise HTTPException( - status_code=500, detail=f"Failed to complete Jira OAuth: {e!s}" - ) from e - - -async def refresh_jira_token( - session: AsyncSession, connector: SearchSourceConnector -) -> SearchSourceConnector: - """ - Refresh the Jira access token for a connector. - - Args: - session: Database session - connector: Jira connector to refresh - - Returns: - Updated connector object - """ - try: - logger.info(f"Refreshing Jira token for connector {connector.id}") - - credentials = JiraAuthCredentialsBase.from_dict(connector.config) - - # Decrypt tokens if they are encrypted - token_encryption = get_token_encryption() - is_encrypted = connector.config.get("_token_encrypted", False) - - refresh_token = credentials.refresh_token - if is_encrypted and refresh_token: - try: - refresh_token = token_encryption.decrypt_token(refresh_token) - except Exception as e: - logger.error(f"Failed to decrypt refresh token: {e!s}") - raise HTTPException( - status_code=500, detail="Failed to decrypt stored refresh token" - ) from e - - if not refresh_token: - raise HTTPException( - status_code=400, - detail="No refresh token available. Please re-authenticate.", - ) - - # Prepare token refresh data - refresh_data = { - "grant_type": "refresh_token", - "client_id": config.JIRA_CLIENT_ID, - "client_secret": config.JIRA_CLIENT_SECRET, - "refresh_token": refresh_token, - } - - async with httpx.AsyncClient() as client: - token_response = await client.post( - TOKEN_URL, - data=refresh_data, - headers={"Content-Type": "application/x-www-form-urlencoded"}, - timeout=30.0, - ) - - if token_response.status_code != 200: - error_detail = token_response.text - try: - error_json = token_response.json() - error_detail = error_json.get("error_description", error_json.get("error", error_detail)) - except Exception: - pass - raise HTTPException( - status_code=400, detail=f"Token refresh failed: {error_detail}" - ) - - token_json = token_response.json() - - # Calculate expiration time (UTC, tz-aware) - expires_at = None - expires_in = token_json.get("expires_in") - if expires_in: - now_utc = datetime.now(UTC) - expires_at = now_utc + timedelta(seconds=int(expires_in)) - - # Encrypt new tokens before storing - access_token = token_json.get("access_token") - new_refresh_token = token_json.get("refresh_token") - - if not access_token: - raise HTTPException( - status_code=400, detail="No access token received from Jira refresh" - ) - - # Update credentials object with encrypted tokens - credentials.access_token = token_encryption.encrypt_token(access_token) - if new_refresh_token: - credentials.refresh_token = token_encryption.encrypt_token( - new_refresh_token - ) - credentials.expires_in = expires_in - credentials.expires_at = expires_at - credentials.scope = token_json.get("scope") - - # Preserve cloud_id and base_url - if not credentials.cloud_id: - credentials.cloud_id = connector.config.get("cloud_id") - if not credentials.base_url: - credentials.base_url = connector.config.get("base_url") - - # Update connector config with encrypted tokens - credentials_dict = credentials.to_dict() - credentials_dict["_token_encrypted"] = True - connector.config = credentials_dict - await session.commit() - await session.refresh(connector) - - logger.info(f"Successfully refreshed Jira token for connector {connector.id}") - - return connector - except HTTPException: - raise - except Exception as e: - logger.error(f"Failed to refresh Jira token: {e!s}", exc_info=True) - raise HTTPException( - status_code=500, detail=f"Failed to refresh Jira token: {e!s}" - ) from e - diff --git a/surfsense_backend/app/schemas/jira_auth_credentials.py b/surfsense_backend/app/schemas/jira_auth_credentials.py deleted file mode 100644 index 23d1ffcbf..000000000 --- a/surfsense_backend/app/schemas/jira_auth_credentials.py +++ /dev/null @@ -1,73 +0,0 @@ -from datetime import UTC, datetime - -from pydantic import BaseModel, field_validator - - -class JiraAuthCredentialsBase(BaseModel): - access_token: str - refresh_token: str | None = None - token_type: str = "Bearer" - expires_in: int | None = None - expires_at: datetime | None = None - scope: str | None = None - cloud_id: str | None = None - base_url: str | None = None - - @property - def is_expired(self) -> bool: - """Check if the credentials have expired.""" - if self.expires_at is None: - return False - return self.expires_at <= datetime.now(UTC) - - @property - def is_refreshable(self) -> bool: - """Check if the credentials can be refreshed.""" - return self.refresh_token is not None - - def to_dict(self) -> dict: - """Convert credentials to dictionary for storage.""" - return { - "access_token": self.access_token, - "refresh_token": self.refresh_token, - "token_type": self.token_type, - "expires_in": self.expires_in, - "expires_at": self.expires_at.isoformat() if self.expires_at else None, - "scope": self.scope, - "cloud_id": self.cloud_id, - "base_url": self.base_url, - } - - @classmethod - def from_dict(cls, data: dict) -> "JiraAuthCredentialsBase": - """Create credentials from dictionary.""" - expires_at = None - if data.get("expires_at"): - expires_at = datetime.fromisoformat(data["expires_at"]) - - return cls( - access_token=data["access_token"], - refresh_token=data.get("refresh_token"), - token_type=data.get("token_type", "Bearer"), - expires_in=data.get("expires_in"), - expires_at=expires_at, - scope=data.get("scope"), - cloud_id=data.get("cloud_id"), - base_url=data.get("base_url"), - ) - - @field_validator("expires_at", mode="before") - @classmethod - def ensure_aware_utc(cls, v): - # Strings like "2025-08-26T14:46:57.367184" - if isinstance(v, str): - # add +00:00 if missing tz info - if v.endswith("Z"): - return datetime.fromisoformat(v.replace("Z", "+00:00")) - dt = datetime.fromisoformat(v) - return dt if dt.tzinfo else dt.replace(tzinfo=UTC) - # datetime objects - if isinstance(v, datetime): - return v if v.tzinfo else v.replace(tzinfo=UTC) - return v - diff --git a/surfsense_backend/app/tasks/connector_indexers/jira_indexer.py b/surfsense_backend/app/tasks/connector_indexers/jira_indexer.py index 616927e6f..8c56b10ab 100644 --- a/surfsense_backend/app/tasks/connector_indexers/jira_indexer.py +++ b/surfsense_backend/app/tasks/connector_indexers/jira_indexer.py @@ -84,137 +84,31 @@ async def index_jira_issues( return 0, f"Connector with ID {connector_id} not found" # Get the Jira credentials from the connector config - # Support both OAuth (preferred) and legacy API token authentication - config_data = connector.config.copy() - is_oauth = config_data.get("_token_encrypted", False) or config_data.get("access_token") + jira_email = connector.config.get("JIRA_EMAIL") + jira_api_token = connector.config.get("JIRA_API_TOKEN") + jira_base_url = connector.config.get("JIRA_BASE_URL") - if is_oauth: - # OAuth 2.0 authentication - from app.utils.oauth_security import TokenEncryption - - if not config.SECRET_KEY: - await task_logger.log_task_failure( - log_entry, - f"SECRET_KEY not configured but tokens are marked as encrypted for connector {connector_id}", - "Missing SECRET_KEY for token decryption", - {"error_type": "MissingSecretKey"}, - ) - return 0, "SECRET_KEY not configured but tokens are marked as encrypted" - - try: - token_encryption = TokenEncryption(config.SECRET_KEY) - - # Decrypt access_token - if config_data.get("access_token"): - config_data["access_token"] = token_encryption.decrypt_token( - config_data["access_token"] - ) - logger.info( - f"Decrypted Jira access token for connector {connector_id}" - ) - - # Decrypt refresh_token if present - if config_data.get("refresh_token"): - config_data["refresh_token"] = token_encryption.decrypt_token( - config_data["refresh_token"] - ) - logger.info( - f"Decrypted Jira refresh token for connector {connector_id}" - ) - except Exception as e: - await task_logger.log_task_failure( - log_entry, - f"Failed to decrypt Jira tokens for connector {connector_id}: {e!s}", - "Token decryption failed", - {"error_type": "TokenDecryptionError"}, - ) - return 0, f"Failed to decrypt Jira tokens: {e!s}" - - try: - from app.schemas.jira_auth_credentials import JiraAuthCredentialsBase - credentials = JiraAuthCredentialsBase.from_dict(config_data) - except Exception as e: - await task_logger.log_task_failure( - log_entry, - f"Invalid Jira OAuth credentials in connector {connector_id}", - str(e), - {"error_type": "InvalidCredentials"}, - ) - return 0, f"Invalid Jira OAuth credentials: {e!s}" - - # Check if credentials are expired and refresh if needed - if credentials.is_expired: - await task_logger.log_task_progress( - log_entry, - f"Jira credentials expired for connector {connector_id}, refreshing token", - {"stage": "token_refresh"}, - ) - - from app.routes.jira_add_connector_route import refresh_jira_token - - try: - connector = await refresh_jira_token(session, connector) - # Re-fetch credentials after refresh - config_data = connector.config.copy() - if config_data.get("access_token"): - config_data["access_token"] = token_encryption.decrypt_token( - config_data["access_token"] - ) - credentials = JiraAuthCredentialsBase.from_dict(config_data) - except Exception as e: - await task_logger.log_task_failure( - log_entry, - f"Failed to refresh Jira token for connector {connector_id}: {e!s}", - "Token refresh failed", - {"error_type": "TokenRefreshError"}, - ) - return 0, f"Failed to refresh Jira token: {e!s}" - - # Initialize Jira client with OAuth credentials - await task_logger.log_task_progress( + if not jira_email or not jira_api_token or not jira_base_url: + await task_logger.log_task_failure( log_entry, - f"Initializing Jira client with OAuth for connector {connector_id}", - {"stage": "client_initialization"}, + f"Jira credentials not found in connector config for connector {connector_id}", + "Missing Jira credentials", + {"error_type": "MissingCredentials"}, ) + return 0, "Jira credentials not found in connector config" - jira_client = JiraConnector( - base_url=credentials.base_url, - access_token=credentials.access_token, - cloud_id=credentials.cloud_id, - ) - else: - # Legacy API token authentication - jira_email = config_data.get("JIRA_EMAIL") - jira_api_token = config_data.get("JIRA_API_TOKEN") - jira_base_url = config_data.get("JIRA_BASE_URL") + # Initialize Jira client + await task_logger.log_task_progress( + log_entry, + f"Initializing Jira client for connector {connector_id}", + {"stage": "client_initialization"}, + ) - if not jira_email or not jira_api_token or not jira_base_url: - await task_logger.log_task_failure( - log_entry, - f"Jira credentials not found in connector config for connector {connector_id}", - "Missing Jira credentials", - {"error_type": "MissingCredentials"}, - ) - return 0, "Jira credentials not found in connector config" - - # Initialize Jira client with legacy credentials - await task_logger.log_task_progress( - log_entry, - f"Initializing Jira client with API token for connector {connector_id}", - {"stage": "client_initialization"}, - ) - - jira_client = JiraConnector( - base_url=jira_base_url, email=jira_email, api_token=jira_api_token - ) + jira_client = JiraConnector( + base_url=jira_base_url, email=jira_email, api_token=jira_api_token + ) # Calculate date range - # Handle "undefined" strings from frontend - if start_date == "undefined" or start_date == "": - start_date = None - if end_date == "undefined" or end_date == "": - end_date = None - start_date_str, end_date_str = calculate_date_range( connector, start_date, end_date, default_days_back=365 ) diff --git a/surfsense_backend/app/utils/validators.py b/surfsense_backend/app/utils/validators.py index d1f416339..f1620c0e5 100644 --- a/surfsense_backend/app/utils/validators.py +++ b/surfsense_backend/app/utils/validators.py @@ -538,13 +538,13 @@ def validate_connector_config( }, }, # "DISCORD_CONNECTOR": {"required": ["DISCORD_BOT_TOKEN"], "validators": {}}, - # "JIRA_CONNECTOR": { - # "required": ["JIRA_EMAIL", "JIRA_API_TOKEN", "JIRA_BASE_URL"], - # "validators": { - # "JIRA_EMAIL": lambda: validate_email_field("JIRA_EMAIL", "JIRA"), - # "JIRA_BASE_URL": lambda: validate_url_field("JIRA_BASE_URL", "JIRA"), - # }, - # }, + "JIRA_CONNECTOR": { + "required": ["JIRA_EMAIL", "JIRA_API_TOKEN", "JIRA_BASE_URL"], + "validators": { + "JIRA_EMAIL": lambda: validate_email_field("JIRA_EMAIL", "JIRA"), + "JIRA_BASE_URL": lambda: validate_url_field("JIRA_BASE_URL", "JIRA"), + }, + }, "CONFLUENCE_CONNECTOR": { "required": [ "CONFLUENCE_BASE_URL", diff --git a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/jira-connect-form.tsx b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/jira-connect-form.tsx new file mode 100644 index 000000000..0499554b4 --- /dev/null +++ b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/jira-connect-form.tsx @@ -0,0 +1,450 @@ +"use client"; + +import { zodResolver } from "@hookform/resolvers/zod"; +import { Info } from "lucide-react"; +import type { FC } from "react"; +import { useRef, useState } from "react"; +import { useForm } from "react-hook-form"; +import * as z from "zod"; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@/components/ui/accordion"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Switch } from "@/components/ui/switch"; +import { EnumConnectorName } from "@/contracts/enums/connector"; +import { DateRangeSelector } from "../../components/date-range-selector"; +import { getConnectorBenefits } from "../connector-benefits"; +import type { ConnectFormProps } from "../index"; + +const jiraConnectorFormSchema = z.object({ + name: z.string().min(3, { + message: "Connector name must be at least 3 characters.", + }), + base_url: z.string().url({ message: "Please enter a valid Jira base URL." }), + email: z.string().email({ message: "Please enter a valid email address." }), + api_token: z.string().min(10, { + message: "Jira API Token is required and must be valid.", + }), +}); + +type JiraConnectorFormValues = z.infer; + +export const JiraConnectForm: FC = ({ onSubmit, isSubmitting }) => { + const isSubmittingRef = useRef(false); + const [startDate, setStartDate] = useState(undefined); + const [endDate, setEndDate] = useState(undefined); + const [periodicEnabled, setPeriodicEnabled] = useState(false); + const [frequencyMinutes, setFrequencyMinutes] = useState("1440"); + const form = useForm({ + resolver: zodResolver(jiraConnectorFormSchema), + defaultValues: { + name: "Jira Connector", + base_url: "", + email: "", + api_token: "", + }, + }); + + const handleSubmit = async (values: JiraConnectorFormValues) => { + // Prevent multiple submissions + if (isSubmittingRef.current || isSubmitting) { + return; + } + + isSubmittingRef.current = true; + try { + await onSubmit({ + name: values.name, + connector_type: EnumConnectorName.JIRA_CONNECTOR, + config: { + JIRA_BASE_URL: values.base_url, + JIRA_EMAIL: values.email, + JIRA_API_TOKEN: values.api_token, + }, + is_indexable: true, + last_indexed_at: null, + periodic_indexing_enabled: periodicEnabled, + indexing_frequency_minutes: periodicEnabled ? parseInt(frequencyMinutes, 10) : null, + next_scheduled_at: null, + startDate, + endDate, + periodicEnabled, + frequencyMinutes, + }); + } finally { + isSubmittingRef.current = false; + } + }; + + return ( +
+ + +
+ API Token Required + + You'll need a Jira API Token to use this connector. You can create one from{" "} + + Atlassian Account Settings + + +
+
+ +
+
+ + ( + + Connector Name + + + + + A friendly name to identify this connector. + + + + )} + /> + + ( + + Jira Base URL + + + + + The base URL of your Jira instance (e.g., https://your-domain.atlassian.net). + + + + )} + /> + + ( + + Email Address + + + + + The email address associated with your Atlassian account. + + + + )} + /> + + ( + + API Token + + + + + Your Jira API Token will be encrypted and stored securely. + + + + )} + /> + + {/* Indexing Configuration */} +
+

Indexing Configuration

+ + {/* Date Range Selector */} + + + {/* Periodic Sync Config */} +
+
+
+

Enable Periodic Sync

+

+ Automatically re-index at regular intervals +

+
+ +
+ + {periodicEnabled && ( +
+
+ + +
+
+ )} +
+
+ + +
+ + {/* What you get section */} + {getConnectorBenefits(EnumConnectorName.JIRA_CONNECTOR) && ( +
+

What you get with Jira integration:

+
    + {getConnectorBenefits(EnumConnectorName.JIRA_CONNECTOR)?.map((benefit) => ( +
  • {benefit}
  • + ))} +
+
+ )} + + {/* Documentation Section */} + + + + Documentation + + +
+

How it works

+

+ The Jira connector uses the Jira REST API with Basic Authentication to fetch all + issues and comments that your account has access to within your Jira instance. +

+
    +
  • + For follow up indexing runs, the connector retrieves issues and comments that have + been updated since the last indexing attempt. +
  • +
  • + Indexing is configured to run periodically, so updates should appear in your + search results within minutes. +
  • +
+
+ +
+
+

Authorization

+ + + + Read-Only Access is Sufficient + + + You only need read access for this connector to work. The API Token will only be + used to read your Jira data. + + + +
+
+

+ Step 1: Create an API Token +

+
    +
  1. Log in to your Atlassian account
  2. +
  3. + Navigate to{" "} + + https://id.atlassian.com/manage-profile/security/api-tokens + {" "} + in your browser. +
  4. +
  5. + Click Create API token +
  6. +
  7. Enter a label for your token (like "SurfSense Connector")
  8. +
  9. + Click Create +
  10. +
  11. Copy the generated token as it will only be shown once
  12. +
+
+ +
+

+ Step 2: Grant necessary access +

+

+ The API Token will have access to all projects and issues that your user + account can see. Make sure your account has appropriate permissions for the + projects you want to index. +

+ + + Data Privacy + + Only issues, comments, and basic metadata will be indexed. Jira attachments + and linked files are not indexed by this connector. + + +
+
+
+
+ +
+
+

Indexing

+
    +
  1. + Navigate to the Connector Dashboard and select the Jira{" "} + Connector. +
  2. +
  3. + Enter your Jira Instance URL (e.g., + https://yourcompany.atlassian.net) +
  4. +
  5. + Enter your Email Address associated with your Atlassian account +
  6. +
  7. + Place your API Token in the form field. +
  8. +
  9. + Click Connect to establish the connection. +
  10. +
  11. Once connected, your Jira issues will be indexed automatically.
  12. +
+ + + + What Gets Indexed + +

The Jira connector indexes the following data:

+
    +
  • Issue keys and summaries (e.g., PROJ-123)
  • +
  • Issue descriptions
  • +
  • Issue comments and discussion threads
  • +
  • Issue status, priority, and type information
  • +
  • Assignee and reporter information
  • +
  • Project information
  • +
+
+
+
+
+
+
+
+
+ ); +}; diff --git a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/index.tsx b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/index.tsx index cda17ddfc..81e5ee03f 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/index.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/index.tsx @@ -6,6 +6,7 @@ import { ClickUpConnectForm } from "./components/clickup-connect-form"; import { ConfluenceConnectForm } from "./components/confluence-connect-form"; import { ElasticsearchConnectForm } from "./components/elasticsearch-connect-form"; import { GithubConnectForm } from "./components/github-connect-form"; +import { JiraConnectForm } from "./components/jira-connect-form"; import { LinkupApiConnectForm } from "./components/linkup-api-connect-form"; import { LumaConnectForm } from "./components/luma-connect-form"; import { SearxngConnectForm } from "./components/searxng-connect-form"; @@ -54,6 +55,8 @@ export function getConnectFormComponent(connectorType: string): ConnectFormCompo return BookStackConnectForm; case "GITHUB_CONNECTOR": return GithubConnectForm; + case "JIRA_CONNECTOR": + return JiraConnectForm; case "CLICKUP_CONNECTOR": return ClickUpConnectForm; case "LUMA_CONNECTOR": diff --git a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/jira-config.tsx b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/jira-config.tsx index 158dfdf13..3ef16bdb4 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/jira-config.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/jira-config.tsx @@ -1,6 +1,6 @@ "use client"; -import { Info, KeyRound } from "lucide-react"; +import { KeyRound } from "lucide-react"; import type { FC } from "react"; import { useEffect, useState } from "react"; import { Input } from "@/components/ui/input"; @@ -12,9 +12,6 @@ export interface JiraConfigProps extends ConnectorConfigProps { } export const JiraConfig: FC = ({ connector, onConfigChange, onNameChange }) => { - // Check if this is an OAuth connector (has access_token or _token_encrypted flag) - const isOAuth = !!(connector.config?.access_token || connector.config?._token_encrypted); - const [baseUrl, setBaseUrl] = useState((connector.config?.JIRA_BASE_URL as string) || ""); const [email, setEmail] = useState((connector.config?.JIRA_EMAIL as string) || ""); const [apiToken, setApiToken] = useState( @@ -22,18 +19,16 @@ export const JiraConfig: FC = ({ connector, onConfigChange, onN ); const [name, setName] = useState(connector.name || ""); - // Update values when connector changes (only for legacy connectors) + // Update values when connector changes useEffect(() => { - if (!isOAuth) { - const url = (connector.config?.JIRA_BASE_URL as string) || ""; - const emailVal = (connector.config?.JIRA_EMAIL as string) || ""; - const token = (connector.config?.JIRA_API_TOKEN as string) || ""; - setBaseUrl(url); - setEmail(emailVal); - setApiToken(token); - } + const url = (connector.config?.JIRA_BASE_URL as string) || ""; + const emailVal = (connector.config?.JIRA_EMAIL as string) || ""; + const token = (connector.config?.JIRA_API_TOKEN as string) || ""; + setBaseUrl(url); + setEmail(emailVal); + setApiToken(token); setName(connector.name || ""); - }, [connector.config, connector.name, isOAuth]); + }, [connector.config, connector.name]); const handleBaseUrlChange = (value: string) => { setBaseUrl(value); @@ -72,34 +67,6 @@ export const JiraConfig: FC = ({ connector, onConfigChange, onN } }; - // For OAuth connectors, show simple info message - if (isOAuth) { - const baseUrl = (connector.config?.base_url as string) || "Unknown"; - return ( -
- {/* OAuth Info */} -
-
- -
-
-

Connected via OAuth

-

- This connector is authenticated using OAuth 2.0. Your Jira instance is: -

-

- {baseUrl} -

-

- To update your connection, disconnect and reconnect through the OAuth flow. -

-
-
-
- ); - } - - // For legacy API token connectors, show the form return (
{/* Connector Name */} diff --git a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-connect-view.tsx b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-connect-view.tsx index 7b0c3e82f..3ba03f956 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-connect-view.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-connect-view.tsx @@ -55,6 +55,7 @@ export const ConnectorConnectView: FC = ({ CONFLUENCE_CONNECTOR: "confluence-connect-form", BOOKSTACK_CONNECTOR: "bookstack-connect-form", GITHUB_CONNECTOR: "github-connect-form", + JIRA_CONNECTOR: "jira-connect-form", CLICKUP_CONNECTOR: "clickup-connect-form", LUMA_CONNECTOR: "luma-connect-form", CIRCLEBACK_CONNECTOR: "circleback-connect-form", diff --git a/surfsense_web/components/assistant-ui/connector-popup/constants/connector-constants.ts b/surfsense_web/components/assistant-ui/connector-popup/constants/connector-constants.ts index 0e942dd1e..9822ff6e6 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/constants/connector-constants.ts +++ b/surfsense_web/components/assistant-ui/connector-popup/constants/connector-constants.ts @@ -58,13 +58,6 @@ export const OAUTH_CONNECTORS = [ connectorType: EnumConnectorName.DISCORD_CONNECTOR, authEndpoint: "/api/v1/auth/discord/connector/add/", }, - { - id: "jira-connector", - title: "Jira", - description: "Search Jira issues", - connectorType: EnumConnectorName.JIRA_CONNECTOR, - authEndpoint: "/api/v1/auth/jira/connector/add/", - }, ] as const; // Content Sources (tools that extract and import content from external sources) @@ -103,6 +96,12 @@ export const OTHER_CONNECTORS = [ description: "Search repositories", connectorType: EnumConnectorName.GITHUB_CONNECTOR, }, + { + id: "jira-connector", + title: "Jira", + description: "Search Jira issues", + connectorType: EnumConnectorName.JIRA_CONNECTOR, + }, { id: "clickup-connector", title: "ClickUp", diff --git a/surfsense_web/hooks/use-connector-edit-page.ts b/surfsense_web/hooks/use-connector-edit-page.ts index ba4ba6b58..3beb80247 100644 --- a/surfsense_web/hooks/use-connector-edit-page.ts +++ b/surfsense_web/hooks/use-connector-edit-page.ts @@ -447,16 +447,6 @@ export function useConnectorEditPage(connectorId: number, searchSpaceId: string) } break; case "JIRA_CONNECTOR": - // Check if this is an OAuth connector (has access_token or _token_encrypted flag) - const isJiraOAuth = !!(originalConfig.access_token || originalConfig._token_encrypted); - - if (isJiraOAuth) { - // OAuth connectors don't allow editing credentials through the form - // Only allow name changes, which are handled separately - break; - } - - // Legacy API token connector - allow editing credentials if ( formData.JIRA_BASE_URL !== originalConfig.JIRA_BASE_URL || formData.JIRA_EMAIL !== originalConfig.JIRA_EMAIL || From 982b9ceb76c76efe0ea5ba5fa7d1706cc13c83cb Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Tue, 6 Jan 2026 01:01:04 +0530 Subject: [PATCH 20/75] feat: implement Jira OAuth integration and connector routes - Added support for Jira OAuth with new environment variables for client ID, client secret, and redirect URI. - Implemented Jira connector routes for OAuth flow, including authorization and callback handling. - Enhanced Jira connector to support both OAuth 2.0 and legacy API token authentication methods. - Updated Jira indexing logic to utilize OAuth credentials with auto-refresh capabilities. - Removed outdated Jira UI components and adjusted frontend logic to reflect the new integration. --- surfsense_backend/.env.example | 5 + .../app/connectors/jira_connector.py | 120 +++-- surfsense_backend/app/routes/__init__.py | 4 +- .../app/routes/jira_add_connector_route.py | 494 ++++++++++++++++++ .../app/schemas/jira_auth_credentials.py | 72 +++ .../tasks/connector_indexers/jira_indexer.py | 144 ++++- surfsense_backend/app/utils/validators.py | 14 +- .../components/jira-connect-form.tsx | 450 ---------------- .../connector-popup/connect-forms/index.tsx | 3 - .../components/jira-config.tsx | 51 +- .../views/connector-connect-view.tsx | 1 - .../constants/connector-constants.ts | 13 +- .../hooks/use-connector-edit-page.ts | 13 +- 13 files changed, 855 insertions(+), 529 deletions(-) create mode 100644 surfsense_backend/app/routes/jira_add_connector_route.py create mode 100644 surfsense_backend/app/schemas/jira_auth_credentials.py delete mode 100644 surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/jira-connect-form.tsx diff --git a/surfsense_backend/.env.example b/surfsense_backend/.env.example index d2c667178..a2f662c23 100644 --- a/surfsense_backend/.env.example +++ b/surfsense_backend/.env.example @@ -50,6 +50,11 @@ DISCORD_CLIENT_SECRET=your_discord_client_secret_here DISCORD_REDIRECT_URI=http://localhost:8000/api/v1/auth/discord/connector/callback DISCORD_BOT_TOKEN=your_bot_token_from_developer_portal +# Jira OAuth Configuration +JIRA_CLIENT_ID=our_jira_client_id +JIRA_CLIENT_SECRET=your_jira_client_secret +JIRA_REDIRECT_URI=http://localhost:8000/api/v1/auth/jira/connector/callback + # OAuth for Linear Connector LINEAR_CLIENT_ID=your_linear_client_id LINEAR_CLIENT_SECRET=your_linear_client_secret diff --git a/surfsense_backend/app/connectors/jira_connector.py b/surfsense_backend/app/connectors/jira_connector.py index e73198e79..7bc8f2f03 100644 --- a/surfsense_backend/app/connectors/jira_connector.py +++ b/surfsense_backend/app/connectors/jira_connector.py @@ -3,6 +3,7 @@ Jira Connector Module A module for retrieving data from Jira. Allows fetching issue lists and their comments, projects and more. +Supports both OAuth 2.0 (preferred) and legacy API token authentication. """ import base64 @@ -18,6 +19,8 @@ class JiraConnector: def __init__( self, base_url: str | None = None, + access_token: str | None = None, + cloud_id: str | None = None, email: str | None = None, api_token: str | None = None, ): @@ -25,18 +28,39 @@ class JiraConnector: Initialize the JiraConnector class. Args: - base_url: Jira instance base URL (e.g., 'https://yourcompany.atlassian.net') (optional) - email: Jira account email address (optional) - api_token: Jira API token (optional) + base_url: Jira instance base URL (e.g., 'https://yourcompany.atlassian.net') + access_token: OAuth 2.0 access token (preferred method) + cloud_id: Atlassian cloud ID (used with OAuth for API URL construction) + email: Jira account email address (legacy method, used with api_token) + api_token: Jira API token (legacy method, used with email) """ self.base_url = base_url.rstrip("/") if base_url else None + self.access_token = access_token + self.cloud_id = cloud_id self.email = email self.api_token = api_token self.api_version = "3" # Jira Cloud API version + self._use_oauth = access_token is not None + + def set_oauth_credentials( + self, base_url: str, access_token: str, cloud_id: str | None = None + ) -> None: + """ + Set OAuth 2.0 credentials (preferred method). + + Args: + base_url: Jira instance base URL + access_token: OAuth 2.0 access token + cloud_id: Atlassian cloud ID (optional, used for API URL construction) + """ + self.base_url = base_url.rstrip("/") + self.access_token = access_token + self.cloud_id = cloud_id + self._use_oauth = True def set_credentials(self, base_url: str, email: str, api_token: str) -> None: """ - Set the Jira credentials. + Set the Jira credentials (legacy method using API token). Args: base_url: Jira instance base URL @@ -46,50 +70,69 @@ class JiraConnector: self.base_url = base_url.rstrip("/") self.email = email self.api_token = api_token + self._use_oauth = False def set_email(self, email: str) -> None: """ - Set the Jira account email. + Set the Jira account email (legacy method). Args: email: Jira account email address """ self.email = email + self._use_oauth = False def set_api_token(self, api_token: str) -> None: """ - Set the Jira API token. + Set the Jira API token (legacy method). Args: api_token: Jira API token """ self.api_token = api_token + self._use_oauth = False def get_headers(self) -> dict[str, str]: """ - Get headers for Jira API requests using Basic Authentication. + Get headers for Jira API requests. + + Uses OAuth Bearer token if available, otherwise falls back to Basic Auth. Returns: Dictionary of headers Raises: - ValueError: If email, api_token, or base_url have not been set + ValueError: If credentials have not been set """ - if not all([self.base_url, self.email, self.api_token]): - raise ValueError( - "Jira credentials not initialized. Call set_credentials() first." - ) + if self._use_oauth: + # OAuth 2.0 authentication + if not self.base_url or not self.access_token: + raise ValueError( + "Jira OAuth credentials not initialized. Call set_oauth_credentials() first." + ) - # Create Basic Auth header using email:api_token - auth_str = f"{self.email}:{self.api_token}" - auth_bytes = auth_str.encode("utf-8") - auth_header = "Basic " + base64.b64encode(auth_bytes).decode("ascii") + return { + "Content-Type": "application/json", + "Authorization": f"Bearer {self.access_token}", + "Accept": "application/json", + } + else: + # Legacy Basic Auth + if not all([self.base_url, self.email, self.api_token]): + raise ValueError( + "Jira credentials not initialized. Call set_credentials() first." + ) - return { - "Content-Type": "application/json", - "Authorization": auth_header, - "Accept": "application/json", - } + # Create Basic Auth header using email:api_token + auth_str = f"{self.email}:{self.api_token}" + auth_bytes = auth_str.encode("utf-8") + auth_header = "Basic " + base64.b64encode(auth_bytes).decode("ascii") + + return { + "Content-Type": "application/json", + "Authorization": auth_header, + "Accept": "application/json", + } def make_api_request( self, @@ -104,22 +147,26 @@ class JiraConnector: Args: endpoint: API endpoint (without base URL) params: Query parameters for the request (optional) + method: HTTP method (GET or POST) + json_payload: JSON payload for POST requests (optional) Returns: Response data from the API Raises: - ValueError: If email, api_token, or base_url have not been set + ValueError: If credentials have not been set Exception: If the API request fails """ - if not all([self.base_url, self.email, self.api_token]): - raise ValueError( - "Jira credentials not initialized. Call set_credentials() first." - ) - - url = f"{self.base_url}/rest/api/{self.api_version}/{endpoint}" headers = self.get_headers() + # Construct API URL based on authentication method + if self._use_oauth and self.cloud_id: + # Use Atlassian API gateway with cloud_id for OAuth + url = f"https://api.atlassian.com/ex/jira/{self.cloud_id}/rest/api/{self.api_version}/{endpoint}" + else: + # Use direct base URL (works for both OAuth and legacy) + url = f"{self.base_url}/rest/api/{self.api_version}/{endpoint}" + if method.upper() == "POST": response = requests.post( url, headers=headers, json=json_payload, timeout=500 @@ -234,15 +281,24 @@ class JiraConnector: try: # Build JQL query for date range # Query issues that were either created OR updated within the date range + # Use end_date + 1 day with < operator to include the full end date + from datetime import datetime, timedelta + + # Parse end_date and add 1 day for inclusive end date + end_date_obj = datetime.strptime(end_date, "%Y-%m-%d") + end_date_next = (end_date_obj + timedelta(days=1)).strftime("%Y-%m-%d") + + # Check both created and updated dates to catch all relevant issues + # Use 'created' and 'updated' (standard JQL field names) date_filter = ( - f"(createdDate >= '{start_date}' AND createdDate <= '{end_date}')" + f"(created >= '{start_date}' AND created < '{end_date_next}') " + f"OR (updated >= '{start_date}' AND updated < '{end_date_next}')" ) - # TODO : This JQL needs some improvement to work as expected - jql = f"{date_filter}" + jql = f"{date_filter} ORDER BY created DESC" if project_key: jql = ( - f'project = "{project_key}" AND {date_filter} ORDER BY created DESC' + f'project = "{project_key}" AND ({date_filter}) ORDER BY created DESC' ) # Define fields to retrieve diff --git a/surfsense_backend/app/routes/__init__.py b/surfsense_backend/app/routes/__init__.py index b35d743e0..b7c4b2a95 100644 --- a/surfsense_backend/app/routes/__init__.py +++ b/surfsense_backend/app/routes/__init__.py @@ -4,6 +4,7 @@ from .airtable_add_connector_route import ( router as airtable_add_connector_router, ) from .circleback_webhook_route import router as circleback_webhook_router +from .discord_add_connector_route import router as discord_add_connector_router from .documents_routes import router as documents_router from .editor_routes import router as editor_router from .google_calendar_add_connector_route import ( @@ -15,6 +16,7 @@ from .google_drive_add_connector_route import ( from .google_gmail_add_connector_route import ( router as google_gmail_add_connector_router, ) +from .jira_add_connector_route import router as jira_add_connector_router from .linear_add_connector_route import router as linear_add_connector_router from .logs_routes import router as logs_router from .luma_add_connector_route import router as luma_add_connector_router @@ -27,7 +29,6 @@ from .rbac_routes import router as rbac_router from .search_source_connectors_routes import router as search_source_connectors_router from .search_spaces_routes import router as search_spaces_router from .slack_add_connector_route import router as slack_add_connector_router -from .discord_add_connector_route import router as discord_add_connector_router router = APIRouter() @@ -48,6 +49,7 @@ router.include_router(luma_add_connector_router) router.include_router(notion_add_connector_router) router.include_router(slack_add_connector_router) router.include_router(discord_add_connector_router) +router.include_router(jira_add_connector_router) router.include_router(new_llm_config_router) # LLM configs with prompt configuration router.include_router(logs_router) router.include_router(circleback_webhook_router) # Circleback meeting webhooks diff --git a/surfsense_backend/app/routes/jira_add_connector_route.py b/surfsense_backend/app/routes/jira_add_connector_route.py new file mode 100644 index 000000000..5b752912a --- /dev/null +++ b/surfsense_backend/app/routes/jira_add_connector_route.py @@ -0,0 +1,494 @@ +""" +Jira Connector OAuth Routes. + +Handles OAuth 2.0 authentication flow for Jira connector. +Uses Atlassian OAuth 2.0 (3LO) with accessible-resources API to discover Jira instances. +""" + +import logging +from datetime import UTC, datetime, timedelta +from uuid import UUID + +import httpx +from fastapi import APIRouter, Depends, HTTPException, Request +from fastapi.responses import RedirectResponse +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, + SearchSourceConnectorType, + User, + get_async_session, +) +from app.schemas.jira_auth_credentials import JiraAuthCredentialsBase +from app.users import current_active_user +from app.utils.oauth_security import OAuthStateManager, TokenEncryption + +logger = logging.getLogger(__name__) + +router = APIRouter() + +# Atlassian OAuth endpoints +AUTHORIZATION_URL = "https://auth.atlassian.com/authorize" +TOKEN_URL = "https://auth.atlassian.com/oauth/token" +ACCESSIBLE_RESOURCES_URL = "https://api.atlassian.com/oauth/token/accessible-resources" + +# OAuth scopes for Jira +SCOPES = [ + "read:jira-work", + "write:jira-work", + "read:jira-user", + "offline_access", # Required for refresh tokens +] + +# Initialize security utilities +_state_manager = None +_token_encryption = None + + +def get_state_manager() -> OAuthStateManager: + """Get or create OAuth state manager instance.""" + global _state_manager + if _state_manager is None: + if not config.SECRET_KEY: + raise ValueError("SECRET_KEY must be set for OAuth security") + _state_manager = OAuthStateManager(config.SECRET_KEY) + return _state_manager + + +def get_token_encryption() -> TokenEncryption: + """Get or create token encryption instance.""" + global _token_encryption + if _token_encryption is None: + if not config.SECRET_KEY: + raise ValueError("SECRET_KEY must be set for token encryption") + _token_encryption = TokenEncryption(config.SECRET_KEY) + return _token_encryption + + +@router.get("/auth/jira/connector/add") +async def connect_jira(space_id: int, user: User = Depends(current_active_user)): + """ + Initiate Jira OAuth flow. + + Args: + space_id: The search space ID + user: Current authenticated user + + Returns: + Authorization URL for redirect + """ + try: + if not space_id: + raise HTTPException(status_code=400, detail="space_id is required") + + if not config.JIRA_CLIENT_ID: + raise HTTPException(status_code=500, detail="Jira OAuth not configured.") + + if not config.SECRET_KEY: + raise HTTPException( + status_code=500, detail="SECRET_KEY not configured for OAuth security." + ) + + # Generate secure state parameter with HMAC signature + state_manager = get_state_manager() + state_encoded = state_manager.generate_secure_state(space_id, user.id) + + # Build authorization URL + from urllib.parse import urlencode + + auth_params = { + "audience": "api.atlassian.com", + "client_id": config.JIRA_CLIENT_ID, + "scope": " ".join(SCOPES), + "redirect_uri": config.JIRA_REDIRECT_URI, + "state": state_encoded, + "response_type": "code", + "prompt": "consent", # Force consent screen to get refresh token + } + + auth_url = f"{AUTHORIZATION_URL}?{urlencode(auth_params)}" + + logger.info(f"Generated Jira OAuth URL for user {user.id}, space {space_id}") + return {"auth_url": auth_url} + + except Exception as e: + logger.error(f"Failed to initiate Jira OAuth: {e!s}", exc_info=True) + raise HTTPException( + status_code=500, detail=f"Failed to initiate Jira OAuth: {e!s}" + ) from e + + +@router.get("/auth/jira/connector/callback") +async def jira_callback( + request: Request, + code: str | None = None, + error: str | None = None, + state: str | None = None, + session: AsyncSession = Depends(get_async_session), +): + """ + Handle Jira OAuth callback. + + Args: + request: FastAPI request object + code: Authorization code from Atlassian (if user granted access) + error: Error code from Atlassian (if user denied access or error occurred) + state: State parameter containing user/space info + session: Database session + + Returns: + Redirect response to frontend + """ + try: + # Handle OAuth errors (e.g., user denied access) + if error: + logger.warning(f"Jira OAuth error: {error}") + # Try to decode state to get space_id for redirect, but don't fail if it's invalid + space_id = None + if state: + try: + state_manager = get_state_manager() + data = state_manager.validate_state(state) + space_id = data.get("space_id") + except Exception: + # If state is invalid, we'll redirect without space_id + logger.warning("Failed to validate state in error handler") + + # Redirect to frontend with error parameter + if space_id: + return RedirectResponse( + url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&error=jira_oauth_denied" + ) + else: + return RedirectResponse( + url=f"{config.NEXT_FRONTEND_URL}/dashboard?error=jira_oauth_denied" + ) + + # Validate required parameters for successful flow + if not code: + raise HTTPException(status_code=400, detail="Missing authorization code") + if not state: + raise HTTPException(status_code=400, detail="Missing state parameter") + + # Validate and decode state with signature verification + state_manager = get_state_manager() + try: + data = state_manager.validate_state(state) + except HTTPException: + raise + except Exception as e: + raise HTTPException( + status_code=400, detail=f"Invalid state parameter: {e!s}" + ) from e + + user_id = UUID(data["user_id"]) + space_id = data["space_id"] + + # Validate redirect URI (security: ensure it matches configured value) + if not config.JIRA_REDIRECT_URI: + raise HTTPException( + status_code=500, detail="JIRA_REDIRECT_URI not configured" + ) + + # Exchange authorization code for access token + token_data = { + "grant_type": "authorization_code", + "client_id": config.JIRA_CLIENT_ID, + "client_secret": config.JIRA_CLIENT_SECRET, + "code": code, + "redirect_uri": config.JIRA_REDIRECT_URI, + } + + async with httpx.AsyncClient() as client: + token_response = await client.post( + TOKEN_URL, + data=token_data, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + timeout=30.0, + ) + + if token_response.status_code != 200: + error_detail = token_response.text + try: + error_json = token_response.json() + error_detail = error_json.get("error_description", error_json.get("error", error_detail)) + except Exception: + pass + raise HTTPException( + status_code=400, detail=f"Token exchange failed: {error_detail}" + ) + + token_json = token_response.json() + + # Encrypt sensitive tokens before storing + token_encryption = get_token_encryption() + access_token = token_json.get("access_token") + refresh_token = token_json.get("refresh_token") + + if not access_token: + raise HTTPException( + status_code=400, detail="No access token received from Atlassian" + ) + + # Fetch accessible resources to get Jira instance information + async with httpx.AsyncClient() as client: + resources_response = await client.get( + ACCESSIBLE_RESOURCES_URL, + headers={"Authorization": f"Bearer {access_token}"}, + timeout=30.0, + ) + + if resources_response.status_code != 200: + error_detail = resources_response.text + logger.error(f"Failed to fetch accessible resources: {error_detail}") + raise HTTPException( + status_code=400, + detail=f"Failed to fetch Jira instances: {error_detail}", + ) + + resources = resources_response.json() + + # Filter for Jira instances (resources with type "jira" or id field) + jira_instances = [ + r + for r in resources + if r.get("id") and (r.get("name") or r.get("url")) + ] + + if not jira_instances: + raise HTTPException( + status_code=400, + detail="No accessible Jira instances found. Please ensure you have access to at least one Jira instance.", + ) + + # For now, use the first Jira instance + # TODO: Support multiple instances by letting user choose during OAuth + jira_instance = jira_instances[0] + cloud_id = jira_instance["id"] + base_url = jira_instance.get("url") + + # If URL is not provided, construct it from cloud_id + if not base_url: + # Try to extract from name or construct default format + instance_name = jira_instance.get("name", "").lower().replace(" ", "") + if instance_name: + base_url = f"https://{instance_name}.atlassian.net" + else: + # Fallback: use cloud_id directly (though this may not work) + base_url = f"https://{cloud_id}.atlassian.net" + + # Calculate expiration time (UTC, tz-aware) + expires_at = None + expires_in = token_json.get("expires_in") + if expires_in: + now_utc = datetime.now(UTC) + expires_at = now_utc + timedelta(seconds=int(expires_in)) + + # Store the encrypted access token and refresh token in connector config + connector_config = { + "access_token": token_encryption.encrypt_token(access_token), + "refresh_token": token_encryption.encrypt_token(refresh_token) + if refresh_token + else None, + "token_type": token_json.get("token_type", "Bearer"), + "expires_in": expires_in, + "expires_at": expires_at.isoformat() if expires_at else None, + "scope": token_json.get("scope"), + "cloud_id": cloud_id, + "base_url": base_url.rstrip("/") if base_url else None, + # Mark that tokens are encrypted for backward compatibility + "_token_encrypted": True, + } + + # Check if connector already exists for this search space and user + existing_connector_result = await session.execute( + select(SearchSourceConnector).filter( + SearchSourceConnector.search_space_id == space_id, + SearchSourceConnector.user_id == user_id, + SearchSourceConnector.connector_type + == SearchSourceConnectorType.JIRA_CONNECTOR, + ) + ) + existing_connector = existing_connector_result.scalars().first() + + if existing_connector: + # Update existing connector + existing_connector.config = connector_config + existing_connector.name = "Jira Connector" + existing_connector.is_indexable = True + logger.info( + f"Updated existing Jira connector for user {user_id} in space {space_id}" + ) + else: + # Create new connector + new_connector = SearchSourceConnector( + name="Jira Connector", + connector_type=SearchSourceConnectorType.JIRA_CONNECTOR, + is_indexable=True, + config=connector_config, + search_space_id=space_id, + user_id=user_id, + ) + session.add(new_connector) + logger.info( + f"Created new Jira connector for user {user_id} in space {space_id}" + ) + + try: + await session.commit() + logger.info(f"Successfully saved Jira connector for user {user_id}") + + # Redirect to the frontend with success params + return RedirectResponse( + url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&success=true&connector=jira-connector" + ) + + except ValidationError as e: + await session.rollback() + raise HTTPException( + status_code=422, detail=f"Validation error: {e!s}" + ) from e + except IntegrityError as e: + await session.rollback() + raise HTTPException( + status_code=409, + detail=f"Integrity error: A connector with this type already exists. {e!s}", + ) from e + except Exception as e: + logger.error(f"Failed to create search source connector: {e!s}") + await session.rollback() + raise HTTPException( + status_code=500, + detail=f"Failed to create search source connector: {e!s}", + ) from e + + except HTTPException: + raise + except Exception as e: + logger.error(f"Failed to complete Jira OAuth: {e!s}", exc_info=True) + raise HTTPException( + status_code=500, detail=f"Failed to complete Jira OAuth: {e!s}" + ) from e + + +async def refresh_jira_token( + session: AsyncSession, connector: SearchSourceConnector +) -> SearchSourceConnector: + """ + Refresh the Jira access token for a connector. + + Args: + session: Database session + connector: Jira connector to refresh + + Returns: + Updated connector object + """ + try: + logger.info(f"Refreshing Jira token for connector {connector.id}") + + credentials = JiraAuthCredentialsBase.from_dict(connector.config) + + # Decrypt tokens if they are encrypted + token_encryption = get_token_encryption() + is_encrypted = connector.config.get("_token_encrypted", False) + + refresh_token = credentials.refresh_token + if is_encrypted and refresh_token: + try: + refresh_token = token_encryption.decrypt_token(refresh_token) + except Exception as e: + logger.error(f"Failed to decrypt refresh token: {e!s}") + raise HTTPException( + status_code=500, detail="Failed to decrypt stored refresh token" + ) from e + + if not refresh_token: + raise HTTPException( + status_code=400, + detail="No refresh token available. Please re-authenticate.", + ) + + # Prepare token refresh data + refresh_data = { + "grant_type": "refresh_token", + "client_id": config.JIRA_CLIENT_ID, + "client_secret": config.JIRA_CLIENT_SECRET, + "refresh_token": refresh_token, + } + + async with httpx.AsyncClient() as client: + token_response = await client.post( + TOKEN_URL, + data=refresh_data, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + timeout=30.0, + ) + + if token_response.status_code != 200: + error_detail = token_response.text + try: + error_json = token_response.json() + error_detail = error_json.get("error_description", error_json.get("error", error_detail)) + except Exception: + pass + raise HTTPException( + status_code=400, detail=f"Token refresh failed: {error_detail}" + ) + + token_json = token_response.json() + + # Calculate expiration time (UTC, tz-aware) + expires_at = None + expires_in = token_json.get("expires_in") + if expires_in: + now_utc = datetime.now(UTC) + expires_at = now_utc + timedelta(seconds=int(expires_in)) + + # Encrypt new tokens before storing + access_token = token_json.get("access_token") + new_refresh_token = token_json.get("refresh_token") + + if not access_token: + raise HTTPException( + status_code=400, detail="No access token received from Jira refresh" + ) + + # Update credentials object with encrypted tokens + credentials.access_token = token_encryption.encrypt_token(access_token) + if new_refresh_token: + credentials.refresh_token = token_encryption.encrypt_token( + new_refresh_token + ) + credentials.expires_in = expires_in + credentials.expires_at = expires_at + credentials.scope = token_json.get("scope") + + # Preserve cloud_id and base_url + if not credentials.cloud_id: + credentials.cloud_id = connector.config.get("cloud_id") + if not credentials.base_url: + credentials.base_url = connector.config.get("base_url") + + # Update connector config with encrypted tokens + credentials_dict = credentials.to_dict() + credentials_dict["_token_encrypted"] = True + connector.config = credentials_dict + await session.commit() + await session.refresh(connector) + + logger.info(f"Successfully refreshed Jira token for connector {connector.id}") + + return connector + except HTTPException: + raise + except Exception as e: + logger.error(f"Failed to refresh Jira token: {e!s}", exc_info=True) + raise HTTPException( + status_code=500, detail=f"Failed to refresh Jira token: {e!s}" + ) from e diff --git a/surfsense_backend/app/schemas/jira_auth_credentials.py b/surfsense_backend/app/schemas/jira_auth_credentials.py new file mode 100644 index 000000000..0e1cfdee2 --- /dev/null +++ b/surfsense_backend/app/schemas/jira_auth_credentials.py @@ -0,0 +1,72 @@ +from datetime import UTC, datetime + +from pydantic import BaseModel, field_validator + + +class JiraAuthCredentialsBase(BaseModel): + access_token: str + refresh_token: str | None = None + token_type: str = "Bearer" + expires_in: int | None = None + expires_at: datetime | None = None + scope: str | None = None + cloud_id: str | None = None + base_url: str | None = None + + @property + def is_expired(self) -> bool: + """Check if the credentials have expired.""" + if self.expires_at is None: + return False + return self.expires_at <= datetime.now(UTC) + + @property + def is_refreshable(self) -> bool: + """Check if the credentials can be refreshed.""" + return self.refresh_token is not None + + def to_dict(self) -> dict: + """Convert credentials to dictionary for storage.""" + return { + "access_token": self.access_token, + "refresh_token": self.refresh_token, + "token_type": self.token_type, + "expires_in": self.expires_in, + "expires_at": self.expires_at.isoformat() if self.expires_at else None, + "scope": self.scope, + "cloud_id": self.cloud_id, + "base_url": self.base_url, + } + + @classmethod + def from_dict(cls, data: dict) -> "JiraAuthCredentialsBase": + """Create credentials from dictionary.""" + expires_at = None + if data.get("expires_at"): + expires_at = datetime.fromisoformat(data["expires_at"]) + + return cls( + access_token=data["access_token"], + refresh_token=data.get("refresh_token"), + token_type=data.get("token_type", "Bearer"), + expires_in=data.get("expires_in"), + expires_at=expires_at, + scope=data.get("scope"), + cloud_id=data.get("cloud_id"), + base_url=data.get("base_url"), + ) + + @field_validator("expires_at", mode="before") + @classmethod + def ensure_aware_utc(cls, v): + # Strings like "2025-08-26T14:46:57.367184" + if isinstance(v, str): + # add +00:00 if missing tz info + if v.endswith("Z"): + return datetime.fromisoformat(v.replace("Z", "+00:00")) + dt = datetime.fromisoformat(v) + return dt if dt.tzinfo else dt.replace(tzinfo=UTC) + # datetime objects + if isinstance(v, datetime): + return v if v.tzinfo else v.replace(tzinfo=UTC) + return v diff --git a/surfsense_backend/app/tasks/connector_indexers/jira_indexer.py b/surfsense_backend/app/tasks/connector_indexers/jira_indexer.py index 8c56b10ab..0bb54aea6 100644 --- a/surfsense_backend/app/tasks/connector_indexers/jira_indexer.py +++ b/surfsense_backend/app/tasks/connector_indexers/jira_indexer.py @@ -84,31 +84,137 @@ async def index_jira_issues( return 0, f"Connector with ID {connector_id} not found" # Get the Jira credentials from the connector config - jira_email = connector.config.get("JIRA_EMAIL") - jira_api_token = connector.config.get("JIRA_API_TOKEN") - jira_base_url = connector.config.get("JIRA_BASE_URL") + # Support both OAuth (preferred) and legacy API token authentication + config_data = connector.config.copy() + is_oauth = config_data.get("_token_encrypted", False) or config_data.get("access_token") - if not jira_email or not jira_api_token or not jira_base_url: - await task_logger.log_task_failure( + if is_oauth: + # OAuth 2.0 authentication + from app.utils.oauth_security import TokenEncryption + + if not config.SECRET_KEY: + await task_logger.log_task_failure( + log_entry, + f"SECRET_KEY not configured but tokens are marked as encrypted for connector {connector_id}", + "Missing SECRET_KEY for token decryption", + {"error_type": "MissingSecretKey"}, + ) + return 0, "SECRET_KEY not configured but tokens are marked as encrypted" + + try: + token_encryption = TokenEncryption(config.SECRET_KEY) + + # Decrypt access_token + if config_data.get("access_token"): + config_data["access_token"] = token_encryption.decrypt_token( + config_data["access_token"] + ) + logger.info( + f"Decrypted Jira access token for connector {connector_id}" + ) + + # Decrypt refresh_token if present + if config_data.get("refresh_token"): + config_data["refresh_token"] = token_encryption.decrypt_token( + config_data["refresh_token"] + ) + logger.info( + f"Decrypted Jira refresh token for connector {connector_id}" + ) + except Exception as e: + await task_logger.log_task_failure( + log_entry, + f"Failed to decrypt Jira tokens for connector {connector_id}: {e!s}", + "Token decryption failed", + {"error_type": "TokenDecryptionError"}, + ) + return 0, f"Failed to decrypt Jira tokens: {e!s}" + + try: + from app.schemas.jira_auth_credentials import JiraAuthCredentialsBase + credentials = JiraAuthCredentialsBase.from_dict(config_data) + except Exception as e: + await task_logger.log_task_failure( + log_entry, + f"Invalid Jira OAuth credentials in connector {connector_id}", + str(e), + {"error_type": "InvalidCredentials"}, + ) + return 0, f"Invalid Jira OAuth credentials: {e!s}" + + # Check if credentials are expired and refresh if needed + if credentials.is_expired: + await task_logger.log_task_progress( + log_entry, + f"Jira credentials expired for connector {connector_id}, refreshing token", + {"stage": "token_refresh"}, + ) + + from app.routes.jira_add_connector_route import refresh_jira_token + + try: + connector = await refresh_jira_token(session, connector) + # Re-fetch credentials after refresh + config_data = connector.config.copy() + if config_data.get("access_token"): + config_data["access_token"] = token_encryption.decrypt_token( + config_data["access_token"] + ) + credentials = JiraAuthCredentialsBase.from_dict(config_data) + except Exception as e: + await task_logger.log_task_failure( + log_entry, + f"Failed to refresh Jira token for connector {connector_id}: {e!s}", + "Token refresh failed", + {"error_type": "TokenRefreshError"}, + ) + return 0, f"Failed to refresh Jira token: {e!s}" + + # Initialize Jira client with OAuth credentials + await task_logger.log_task_progress( log_entry, - f"Jira credentials not found in connector config for connector {connector_id}", - "Missing Jira credentials", - {"error_type": "MissingCredentials"}, + f"Initializing Jira client with OAuth for connector {connector_id}", + {"stage": "client_initialization"}, ) - return 0, "Jira credentials not found in connector config" - # Initialize Jira client - await task_logger.log_task_progress( - log_entry, - f"Initializing Jira client for connector {connector_id}", - {"stage": "client_initialization"}, - ) + jira_client = JiraConnector( + base_url=credentials.base_url, + access_token=credentials.access_token, + cloud_id=credentials.cloud_id, + ) + else: + # Legacy API token authentication + jira_email = config_data.get("JIRA_EMAIL") + jira_api_token = config_data.get("JIRA_API_TOKEN") + jira_base_url = config_data.get("JIRA_BASE_URL") - jira_client = JiraConnector( - base_url=jira_base_url, email=jira_email, api_token=jira_api_token - ) + if not jira_email or not jira_api_token or not jira_base_url: + await task_logger.log_task_failure( + log_entry, + f"Jira credentials not found in connector config for connector {connector_id}", + "Missing Jira credentials", + {"error_type": "MissingCredentials"}, + ) + return 0, "Jira credentials not found in connector config" + + # Initialize Jira client with legacy credentials + await task_logger.log_task_progress( + log_entry, + f"Initializing Jira client with API token for connector {connector_id}", + {"stage": "client_initialization"}, + ) + + jira_client = JiraConnector( + base_url=jira_base_url, email=jira_email, api_token=jira_api_token + ) # Calculate date range + # Handle "undefined" strings from frontend + if start_date == "undefined" or start_date == "": + start_date = None + if end_date == "undefined" or end_date == "": + end_date = None + start_date_str, end_date_str = calculate_date_range( connector, start_date, end_date, default_days_back=365 ) @@ -422,4 +528,4 @@ async def index_jira_issues( {"error_type": type(e).__name__}, ) logger.error(f"Failed to index JIRA issues: {e!s}", exc_info=True) - return 0, f"Failed to index JIRA issues: {e!s}" + return 0, f"Failed to index JIRA issues: {e!s}" \ No newline at end of file diff --git a/surfsense_backend/app/utils/validators.py b/surfsense_backend/app/utils/validators.py index f1620c0e5..d1f416339 100644 --- a/surfsense_backend/app/utils/validators.py +++ b/surfsense_backend/app/utils/validators.py @@ -538,13 +538,13 @@ def validate_connector_config( }, }, # "DISCORD_CONNECTOR": {"required": ["DISCORD_BOT_TOKEN"], "validators": {}}, - "JIRA_CONNECTOR": { - "required": ["JIRA_EMAIL", "JIRA_API_TOKEN", "JIRA_BASE_URL"], - "validators": { - "JIRA_EMAIL": lambda: validate_email_field("JIRA_EMAIL", "JIRA"), - "JIRA_BASE_URL": lambda: validate_url_field("JIRA_BASE_URL", "JIRA"), - }, - }, + # "JIRA_CONNECTOR": { + # "required": ["JIRA_EMAIL", "JIRA_API_TOKEN", "JIRA_BASE_URL"], + # "validators": { + # "JIRA_EMAIL": lambda: validate_email_field("JIRA_EMAIL", "JIRA"), + # "JIRA_BASE_URL": lambda: validate_url_field("JIRA_BASE_URL", "JIRA"), + # }, + # }, "CONFLUENCE_CONNECTOR": { "required": [ "CONFLUENCE_BASE_URL", diff --git a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/jira-connect-form.tsx b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/jira-connect-form.tsx deleted file mode 100644 index 0499554b4..000000000 --- a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/jira-connect-form.tsx +++ /dev/null @@ -1,450 +0,0 @@ -"use client"; - -import { zodResolver } from "@hookform/resolvers/zod"; -import { Info } from "lucide-react"; -import type { FC } from "react"; -import { useRef, useState } from "react"; -import { useForm } from "react-hook-form"; -import * as z from "zod"; -import { - Accordion, - AccordionContent, - AccordionItem, - AccordionTrigger, -} from "@/components/ui/accordion"; -import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; -import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { Switch } from "@/components/ui/switch"; -import { EnumConnectorName } from "@/contracts/enums/connector"; -import { DateRangeSelector } from "../../components/date-range-selector"; -import { getConnectorBenefits } from "../connector-benefits"; -import type { ConnectFormProps } from "../index"; - -const jiraConnectorFormSchema = z.object({ - name: z.string().min(3, { - message: "Connector name must be at least 3 characters.", - }), - base_url: z.string().url({ message: "Please enter a valid Jira base URL." }), - email: z.string().email({ message: "Please enter a valid email address." }), - api_token: z.string().min(10, { - message: "Jira API Token is required and must be valid.", - }), -}); - -type JiraConnectorFormValues = z.infer; - -export const JiraConnectForm: FC = ({ onSubmit, isSubmitting }) => { - const isSubmittingRef = useRef(false); - const [startDate, setStartDate] = useState(undefined); - const [endDate, setEndDate] = useState(undefined); - const [periodicEnabled, setPeriodicEnabled] = useState(false); - const [frequencyMinutes, setFrequencyMinutes] = useState("1440"); - const form = useForm({ - resolver: zodResolver(jiraConnectorFormSchema), - defaultValues: { - name: "Jira Connector", - base_url: "", - email: "", - api_token: "", - }, - }); - - const handleSubmit = async (values: JiraConnectorFormValues) => { - // Prevent multiple submissions - if (isSubmittingRef.current || isSubmitting) { - return; - } - - isSubmittingRef.current = true; - try { - await onSubmit({ - name: values.name, - connector_type: EnumConnectorName.JIRA_CONNECTOR, - config: { - JIRA_BASE_URL: values.base_url, - JIRA_EMAIL: values.email, - JIRA_API_TOKEN: values.api_token, - }, - is_indexable: true, - last_indexed_at: null, - periodic_indexing_enabled: periodicEnabled, - indexing_frequency_minutes: periodicEnabled ? parseInt(frequencyMinutes, 10) : null, - next_scheduled_at: null, - startDate, - endDate, - periodicEnabled, - frequencyMinutes, - }); - } finally { - isSubmittingRef.current = false; - } - }; - - return ( -
- - -
- API Token Required - - You'll need a Jira API Token to use this connector. You can create one from{" "} - - Atlassian Account Settings - - -
-
- -
-
- - ( - - Connector Name - - - - - A friendly name to identify this connector. - - - - )} - /> - - ( - - Jira Base URL - - - - - The base URL of your Jira instance (e.g., https://your-domain.atlassian.net). - - - - )} - /> - - ( - - Email Address - - - - - The email address associated with your Atlassian account. - - - - )} - /> - - ( - - API Token - - - - - Your Jira API Token will be encrypted and stored securely. - - - - )} - /> - - {/* Indexing Configuration */} -
-

Indexing Configuration

- - {/* Date Range Selector */} - - - {/* Periodic Sync Config */} -
-
-
-

Enable Periodic Sync

-

- Automatically re-index at regular intervals -

-
- -
- - {periodicEnabled && ( -
-
- - -
-
- )} -
-
- - -
- - {/* What you get section */} - {getConnectorBenefits(EnumConnectorName.JIRA_CONNECTOR) && ( -
-

What you get with Jira integration:

-
    - {getConnectorBenefits(EnumConnectorName.JIRA_CONNECTOR)?.map((benefit) => ( -
  • {benefit}
  • - ))} -
-
- )} - - {/* Documentation Section */} - - - - Documentation - - -
-

How it works

-

- The Jira connector uses the Jira REST API with Basic Authentication to fetch all - issues and comments that your account has access to within your Jira instance. -

-
    -
  • - For follow up indexing runs, the connector retrieves issues and comments that have - been updated since the last indexing attempt. -
  • -
  • - Indexing is configured to run periodically, so updates should appear in your - search results within minutes. -
  • -
-
- -
-
-

Authorization

- - - - Read-Only Access is Sufficient - - - You only need read access for this connector to work. The API Token will only be - used to read your Jira data. - - - -
-
-

- Step 1: Create an API Token -

-
    -
  1. Log in to your Atlassian account
  2. -
  3. - Navigate to{" "} - - https://id.atlassian.com/manage-profile/security/api-tokens - {" "} - in your browser. -
  4. -
  5. - Click Create API token -
  6. -
  7. Enter a label for your token (like "SurfSense Connector")
  8. -
  9. - Click Create -
  10. -
  11. Copy the generated token as it will only be shown once
  12. -
-
- -
-

- Step 2: Grant necessary access -

-

- The API Token will have access to all projects and issues that your user - account can see. Make sure your account has appropriate permissions for the - projects you want to index. -

- - - Data Privacy - - Only issues, comments, and basic metadata will be indexed. Jira attachments - and linked files are not indexed by this connector. - - -
-
-
-
- -
-
-

Indexing

-
    -
  1. - Navigate to the Connector Dashboard and select the Jira{" "} - Connector. -
  2. -
  3. - Enter your Jira Instance URL (e.g., - https://yourcompany.atlassian.net) -
  4. -
  5. - Enter your Email Address associated with your Atlassian account -
  6. -
  7. - Place your API Token in the form field. -
  8. -
  9. - Click Connect to establish the connection. -
  10. -
  11. Once connected, your Jira issues will be indexed automatically.
  12. -
- - - - What Gets Indexed - -

The Jira connector indexes the following data:

-
    -
  • Issue keys and summaries (e.g., PROJ-123)
  • -
  • Issue descriptions
  • -
  • Issue comments and discussion threads
  • -
  • Issue status, priority, and type information
  • -
  • Assignee and reporter information
  • -
  • Project information
  • -
-
-
-
-
-
-
-
-
- ); -}; diff --git a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/index.tsx b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/index.tsx index 81e5ee03f..cda17ddfc 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/index.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/index.tsx @@ -6,7 +6,6 @@ import { ClickUpConnectForm } from "./components/clickup-connect-form"; import { ConfluenceConnectForm } from "./components/confluence-connect-form"; import { ElasticsearchConnectForm } from "./components/elasticsearch-connect-form"; import { GithubConnectForm } from "./components/github-connect-form"; -import { JiraConnectForm } from "./components/jira-connect-form"; import { LinkupApiConnectForm } from "./components/linkup-api-connect-form"; import { LumaConnectForm } from "./components/luma-connect-form"; import { SearxngConnectForm } from "./components/searxng-connect-form"; @@ -55,8 +54,6 @@ export function getConnectFormComponent(connectorType: string): ConnectFormCompo return BookStackConnectForm; case "GITHUB_CONNECTOR": return GithubConnectForm; - case "JIRA_CONNECTOR": - return JiraConnectForm; case "CLICKUP_CONNECTOR": return ClickUpConnectForm; case "LUMA_CONNECTOR": diff --git a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/jira-config.tsx b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/jira-config.tsx index 3ef16bdb4..158dfdf13 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/jira-config.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/jira-config.tsx @@ -1,6 +1,6 @@ "use client"; -import { KeyRound } from "lucide-react"; +import { Info, KeyRound } from "lucide-react"; import type { FC } from "react"; import { useEffect, useState } from "react"; import { Input } from "@/components/ui/input"; @@ -12,6 +12,9 @@ export interface JiraConfigProps extends ConnectorConfigProps { } export const JiraConfig: FC = ({ connector, onConfigChange, onNameChange }) => { + // Check if this is an OAuth connector (has access_token or _token_encrypted flag) + const isOAuth = !!(connector.config?.access_token || connector.config?._token_encrypted); + const [baseUrl, setBaseUrl] = useState((connector.config?.JIRA_BASE_URL as string) || ""); const [email, setEmail] = useState((connector.config?.JIRA_EMAIL as string) || ""); const [apiToken, setApiToken] = useState( @@ -19,16 +22,18 @@ export const JiraConfig: FC = ({ connector, onConfigChange, onN ); const [name, setName] = useState(connector.name || ""); - // Update values when connector changes + // Update values when connector changes (only for legacy connectors) useEffect(() => { - const url = (connector.config?.JIRA_BASE_URL as string) || ""; - const emailVal = (connector.config?.JIRA_EMAIL as string) || ""; - const token = (connector.config?.JIRA_API_TOKEN as string) || ""; - setBaseUrl(url); - setEmail(emailVal); - setApiToken(token); + if (!isOAuth) { + const url = (connector.config?.JIRA_BASE_URL as string) || ""; + const emailVal = (connector.config?.JIRA_EMAIL as string) || ""; + const token = (connector.config?.JIRA_API_TOKEN as string) || ""; + setBaseUrl(url); + setEmail(emailVal); + setApiToken(token); + } setName(connector.name || ""); - }, [connector.config, connector.name]); + }, [connector.config, connector.name, isOAuth]); const handleBaseUrlChange = (value: string) => { setBaseUrl(value); @@ -67,6 +72,34 @@ export const JiraConfig: FC = ({ connector, onConfigChange, onN } }; + // For OAuth connectors, show simple info message + if (isOAuth) { + const baseUrl = (connector.config?.base_url as string) || "Unknown"; + return ( +
+ {/* OAuth Info */} +
+
+ +
+
+

Connected via OAuth

+

+ This connector is authenticated using OAuth 2.0. Your Jira instance is: +

+

+ {baseUrl} +

+

+ To update your connection, disconnect and reconnect through the OAuth flow. +

+
+
+
+ ); + } + + // For legacy API token connectors, show the form return (
{/* Connector Name */} diff --git a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-connect-view.tsx b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-connect-view.tsx index 3ba03f956..7b0c3e82f 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-connect-view.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-connect-view.tsx @@ -55,7 +55,6 @@ export const ConnectorConnectView: FC = ({ CONFLUENCE_CONNECTOR: "confluence-connect-form", BOOKSTACK_CONNECTOR: "bookstack-connect-form", GITHUB_CONNECTOR: "github-connect-form", - JIRA_CONNECTOR: "jira-connect-form", CLICKUP_CONNECTOR: "clickup-connect-form", LUMA_CONNECTOR: "luma-connect-form", CIRCLEBACK_CONNECTOR: "circleback-connect-form", diff --git a/surfsense_web/components/assistant-ui/connector-popup/constants/connector-constants.ts b/surfsense_web/components/assistant-ui/connector-popup/constants/connector-constants.ts index 9822ff6e6..0e942dd1e 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/constants/connector-constants.ts +++ b/surfsense_web/components/assistant-ui/connector-popup/constants/connector-constants.ts @@ -58,6 +58,13 @@ export const OAUTH_CONNECTORS = [ connectorType: EnumConnectorName.DISCORD_CONNECTOR, authEndpoint: "/api/v1/auth/discord/connector/add/", }, + { + id: "jira-connector", + title: "Jira", + description: "Search Jira issues", + connectorType: EnumConnectorName.JIRA_CONNECTOR, + authEndpoint: "/api/v1/auth/jira/connector/add/", + }, ] as const; // Content Sources (tools that extract and import content from external sources) @@ -96,12 +103,6 @@ export const OTHER_CONNECTORS = [ description: "Search repositories", connectorType: EnumConnectorName.GITHUB_CONNECTOR, }, - { - id: "jira-connector", - title: "Jira", - description: "Search Jira issues", - connectorType: EnumConnectorName.JIRA_CONNECTOR, - }, { id: "clickup-connector", title: "ClickUp", diff --git a/surfsense_web/hooks/use-connector-edit-page.ts b/surfsense_web/hooks/use-connector-edit-page.ts index 3beb80247..5eb55bf1c 100644 --- a/surfsense_web/hooks/use-connector-edit-page.ts +++ b/surfsense_web/hooks/use-connector-edit-page.ts @@ -446,7 +446,17 @@ export function useConnectorEditPage(connectorId: number, searchSpaceId: string) }; } break; - case "JIRA_CONNECTOR": + case "JIRA_CONNECTOR": { + // Check if this is an OAuth connector (has access_token or _token_encrypted flag) + const isJiraOAuth = !!(originalConfig.access_token || originalConfig._token_encrypted); + + if (isJiraOAuth) { + // OAuth connectors don't allow editing credentials through the form + // Only allow name changes, which are handled separately + break; + } + + // Legacy API token connector - allow editing credentials if ( formData.JIRA_BASE_URL !== originalConfig.JIRA_BASE_URL || formData.JIRA_EMAIL !== originalConfig.JIRA_EMAIL || @@ -464,6 +474,7 @@ export function useConnectorEditPage(connectorId: number, searchSpaceId: string) }; } break; + } case "LUMA_CONNECTOR": if (formData.LUMA_API_KEY !== originalConfig.LUMA_API_KEY) { if (!formData.LUMA_API_KEY) { From bf8c3bfcf7b00528be03ce3ef81fa575bfc9b82f Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Tue, 6 Jan 2026 01:27:29 +0530 Subject: [PATCH 21/75] feat: add Atlassian OAuth support for Jira and Confluence - Introduced a shared schema for Atlassian OAuth 2.0 credentials, accommodating both Jira and Confluence. - Updated Jira connector routes to utilize the new AtlassianAuthCredentialsBase for handling OAuth tokens. - Enhanced configuration to include new environment variables for Jira OAuth integration. - Refactored token handling in Jira indexing logic to support the new shared credential structure. --- surfsense_backend/app/config/__init__.py | 5 +++++ .../app/routes/jira_add_connector_route.py | 4 ++-- ...tials.py => atlassian_auth_credentials.py} | 19 +++++++++++++++++-- .../tasks/connector_indexers/jira_indexer.py | 6 +++--- 4 files changed, 27 insertions(+), 7 deletions(-) rename surfsense_backend/app/schemas/{jira_auth_credentials.py => atlassian_auth_credentials.py} (81%) diff --git a/surfsense_backend/app/config/__init__.py b/surfsense_backend/app/config/__init__.py index f65a94cc0..4abdf915a 100644 --- a/surfsense_backend/app/config/__init__.py +++ b/surfsense_backend/app/config/__init__.py @@ -95,6 +95,11 @@ class Config: NOTION_CLIENT_SECRET = os.getenv("NOTION_CLIENT_SECRET") NOTION_REDIRECT_URI = os.getenv("NOTION_REDIRECT_URI") + # Jira OAuth + JIRA_CLIENT_ID = os.getenv("JIRA_CLIENT_ID") + JIRA_CLIENT_SECRET = os.getenv("JIRA_CLIENT_SECRET") + JIRA_REDIRECT_URI = os.getenv("JIRA_REDIRECT_URI") + # Linear OAuth LINEAR_CLIENT_ID = os.getenv("LINEAR_CLIENT_ID") LINEAR_CLIENT_SECRET = os.getenv("LINEAR_CLIENT_SECRET") diff --git a/surfsense_backend/app/routes/jira_add_connector_route.py b/surfsense_backend/app/routes/jira_add_connector_route.py index 5b752912a..302a118db 100644 --- a/surfsense_backend/app/routes/jira_add_connector_route.py +++ b/surfsense_backend/app/routes/jira_add_connector_route.py @@ -24,7 +24,7 @@ from app.db import ( User, get_async_session, ) -from app.schemas.jira_auth_credentials import JiraAuthCredentialsBase +from app.schemas.atlassian_auth_credentials import AtlassianAuthCredentialsBase from app.users import current_active_user from app.utils.oauth_security import OAuthStateManager, TokenEncryption @@ -392,7 +392,7 @@ async def refresh_jira_token( try: logger.info(f"Refreshing Jira token for connector {connector.id}") - credentials = JiraAuthCredentialsBase.from_dict(connector.config) + credentials = AtlassianAuthCredentialsBase.from_dict(connector.config) # Decrypt tokens if they are encrypted token_encryption = get_token_encryption() diff --git a/surfsense_backend/app/schemas/jira_auth_credentials.py b/surfsense_backend/app/schemas/atlassian_auth_credentials.py similarity index 81% rename from surfsense_backend/app/schemas/jira_auth_credentials.py rename to surfsense_backend/app/schemas/atlassian_auth_credentials.py index 0e1cfdee2..3290e5d67 100644 --- a/surfsense_backend/app/schemas/jira_auth_credentials.py +++ b/surfsense_backend/app/schemas/atlassian_auth_credentials.py @@ -1,9 +1,23 @@ +""" +Atlassian OAuth 2.0 Authentication Credentials Schema. + +Shared schema for both Jira and Confluence OAuth credentials. +Both products use the same Atlassian OAuth 2.0 (3LO) flow and token structure. +""" + from datetime import UTC, datetime from pydantic import BaseModel, field_validator -class JiraAuthCredentialsBase(BaseModel): +class AtlassianAuthCredentialsBase(BaseModel): + """ + Base model for Atlassian OAuth 2.0 credentials. + + Used for both Jira and Confluence connectors since they share + the same Atlassian OAuth infrastructure and token structure. + """ + access_token: str refresh_token: str | None = None token_type: str = "Bearer" @@ -39,7 +53,7 @@ class JiraAuthCredentialsBase(BaseModel): } @classmethod - def from_dict(cls, data: dict) -> "JiraAuthCredentialsBase": + def from_dict(cls, data: dict) -> "AtlassianAuthCredentialsBase": """Create credentials from dictionary.""" expires_at = None if data.get("expires_at"): @@ -70,3 +84,4 @@ class JiraAuthCredentialsBase(BaseModel): if isinstance(v, datetime): return v if v.tzinfo else v.replace(tzinfo=UTC) return v + diff --git a/surfsense_backend/app/tasks/connector_indexers/jira_indexer.py b/surfsense_backend/app/tasks/connector_indexers/jira_indexer.py index 0bb54aea6..cd7dabeaf 100644 --- a/surfsense_backend/app/tasks/connector_indexers/jira_indexer.py +++ b/surfsense_backend/app/tasks/connector_indexers/jira_indexer.py @@ -131,8 +131,8 @@ async def index_jira_issues( return 0, f"Failed to decrypt Jira tokens: {e!s}" try: - from app.schemas.jira_auth_credentials import JiraAuthCredentialsBase - credentials = JiraAuthCredentialsBase.from_dict(config_data) + from app.schemas.atlassian_auth_credentials import AtlassianAuthCredentialsBase + credentials = AtlassianAuthCredentialsBase.from_dict(config_data) except Exception as e: await task_logger.log_task_failure( log_entry, @@ -160,7 +160,7 @@ async def index_jira_issues( config_data["access_token"] = token_encryption.decrypt_token( config_data["access_token"] ) - credentials = JiraAuthCredentialsBase.from_dict(config_data) + credentials = AtlassianAuthCredentialsBase.from_dict(config_data) except Exception as e: await task_logger.log_task_failure( log_entry, From aac04320236db523a98f6e70235eb90ff08afdca Mon Sep 17 00:00:00 2001 From: "DESKTOP-RTLN3BA\\$punk" Date: Mon, 5 Jan 2026 22:18:25 -0800 Subject: [PATCH 22/75] refactor: update Discord message indexing logic - Enhanced the indexing process for Discord messages to treat each message as an individual document, improving metadata handling and content management. - Replaced the announcement banner component and related state management with a more streamlined approach, removing unnecessary files and simplifying the dashboard layout. - Updated logging messages for clarity and accuracy regarding processed messages. --- .../connector_indexers/discord_indexer.py | 312 ++++++++---------- surfsense_web/app/dashboard/layout.tsx | 2 - surfsense_web/atoms/announcement.atom.ts | 5 - .../components/announcement-banner.tsx | 47 --- 4 files changed, 129 insertions(+), 237 deletions(-) delete mode 100644 surfsense_web/atoms/announcement.atom.ts delete mode 100644 surfsense_web/components/announcement-banner.tsx diff --git a/surfsense_backend/app/tasks/connector_indexers/discord_indexer.py b/surfsense_backend/app/tasks/connector_indexers/discord_indexer.py index b3de1f4b5..5c92d2601 100644 --- a/surfsense_backend/app/tasks/connector_indexers/discord_indexer.py +++ b/surfsense_backend/app/tasks/connector_indexers/discord_indexer.py @@ -11,17 +11,15 @@ from sqlalchemy.ext.asyncio import AsyncSession from app.config import config from app.connectors.discord_connector import DiscordConnector from app.db import Document, DocumentType, SearchSourceConnectorType -from app.services.llm_service import get_user_long_context_llm from app.services.task_logging_service import TaskLoggingService from app.utils.document_converters import ( create_document_chunks, generate_content_hash, - generate_document_summary, generate_unique_identifier_hash, ) from .base import ( - build_document_metadata_string, + build_document_metadata_markdown, check_document_by_unique_identifier, get_connector_by_id, get_current_timestamp, @@ -336,207 +334,155 @@ async def index_discord_messages( documents_skipped += 1 continue - # Convert messages to markdown format - channel_content = ( - f"# Discord Channel: {guild_name} / {channel_name}\n\n" - ) + # Process each message as an individual document (like Slack) for msg in formatted_messages: - user_name = msg.get("author_name", "Unknown User") - timestamp = msg.get("created_at", "Unknown Time") - text = msg.get("content", "") - channel_content += ( - f"## {user_name} ({timestamp})\n\n{text}\n\n---\n\n" + msg_id = msg.get("id", "") + msg_user_name = msg.get("author_name", "Unknown User") + msg_timestamp = msg.get("created_at", "Unknown Time") + msg_text = msg.get("content", "") + + # Format document metadata (similar to Slack) + metadata_sections = [ + ( + "METADATA", + [ + f"GUILD_NAME: {guild_name}", + f"GUILD_ID: {guild_id}", + f"CHANNEL_NAME: {channel_name}", + f"CHANNEL_ID: {channel_id}", + f"MESSAGE_TIMESTAMP: {msg_timestamp}", + f"MESSAGE_USER_NAME: {msg_user_name}", + ], + ), + ( + "CONTENT", + [ + "FORMAT: markdown", + "TEXT_START", + msg_text, + "TEXT_END", + ], + ), + ] + + # Build the document string + combined_document_string = build_document_metadata_markdown( + metadata_sections ) - # Metadata sections - metadata_sections = [ - ( - "METADATA", - [ - f"GUILD_NAME: {guild_name}", - f"GUILD_ID: {guild_id}", - f"CHANNEL_NAME: {channel_name}", - f"CHANNEL_ID: {channel_id}", - f"MESSAGE_COUNT: {len(formatted_messages)}", - ], - ), - ( - "CONTENT", - [ - "FORMAT: markdown", - "TEXT_START", - channel_content, - "TEXT_END", - ], - ), - ] + # Generate unique identifier hash for this Discord message + unique_identifier = f"{channel_id}_{msg_id}" + unique_identifier_hash = generate_unique_identifier_hash( + DocumentType.DISCORD_CONNECTOR, + unique_identifier, + search_space_id, + ) - combined_document_string = build_document_metadata_string( - metadata_sections - ) + # Generate content hash + content_hash = generate_content_hash( + combined_document_string, search_space_id + ) - # Generate unique identifier hash for this Discord channel - unique_identifier_hash = generate_unique_identifier_hash( - DocumentType.DISCORD_CONNECTOR, channel_id, search_space_id - ) + # Check if document with this unique identifier already exists + existing_document = await check_document_by_unique_identifier( + session, unique_identifier_hash + ) - # Generate content hash - content_hash = generate_content_hash( - combined_document_string, search_space_id - ) - - # Check if document with this unique identifier already exists - existing_document = await check_document_by_unique_identifier( - session, unique_identifier_hash - ) - - if existing_document: - # Document exists - check if content has changed - if existing_document.content_hash == content_hash: - logger.info( - f"Document for Discord channel {guild_name}#{channel_name} unchanged. Skipping." - ) - documents_skipped += 1 - continue - else: - # Content has changed - update the existing document - logger.info( - f"Content changed for Discord channel {guild_name}#{channel_name}. Updating document." - ) - - # Get user's long context LLM - user_llm = await get_user_long_context_llm( - session, user_id, search_space_id - ) - if not user_llm: - logger.error( - f"No long context LLM configured for user {user_id}" - ) - skipped_channels.append( - f"{guild_name}#{channel_name} (no LLM configured)" + if existing_document: + # Document exists - check if content has changed + if existing_document.content_hash == content_hash: + logger.info( + f"Document for Discord message {msg_id} in {guild_name}#{channel_name} unchanged. Skipping." ) documents_skipped += 1 continue + else: + # Content has changed - update the existing document + logger.info( + f"Content changed for Discord message {msg_id} in {guild_name}#{channel_name}. Updating document." + ) - # Generate summary with metadata - document_metadata = { - "guild_name": guild_name, - "channel_name": channel_name, - "message_count": len(formatted_messages), - "document_type": "Discord Channel Messages", - "connector_type": "Discord", - } - ( - summary_content, - summary_embedding, - ) = await generate_document_summary( - combined_document_string, - user_llm, - document_metadata, - ) + # Update chunks and embedding + chunks = await create_document_chunks( + combined_document_string + ) + doc_embedding = config.embedding_model_instance.embed( + combined_document_string + ) - # Chunks from channel content - chunks = await create_document_chunks(channel_content) + # Update existing document + existing_document.content = combined_document_string + existing_document.content_hash = content_hash + existing_document.embedding = doc_embedding + existing_document.document_metadata = { + "guild_name": guild_name, + "guild_id": guild_id, + "channel_name": channel_name, + "channel_id": channel_id, + "message_id": msg_id, + "message_timestamp": msg_timestamp, + "message_user_name": msg_user_name, + "indexed_at": datetime.now(UTC).strftime( + "%Y-%m-%d %H:%M:%S" + ), + } - # Update existing document - existing_document.title = ( - f"Discord - {guild_name}#{channel_name}" - ) - existing_document.content = summary_content - existing_document.content_hash = content_hash - existing_document.embedding = summary_embedding - existing_document.document_metadata = { + # Delete old chunks and add new ones + existing_document.chunks = chunks + existing_document.updated_at = get_current_timestamp() + + documents_indexed += 1 + logger.info( + f"Successfully updated Discord message {msg_id}" + ) + continue + + # Document doesn't exist - create new one + # Process chunks + chunks = await create_document_chunks(combined_document_string) + doc_embedding = config.embedding_model_instance.embed( + combined_document_string + ) + + # Create and store new document + document = Document( + search_space_id=search_space_id, + title=f"Discord - {guild_name}#{channel_name}", + document_type=DocumentType.DISCORD_CONNECTOR, + document_metadata={ "guild_name": guild_name, "guild_id": guild_id, "channel_name": channel_name, "channel_id": channel_id, - "message_count": len(formatted_messages), - "start_date": start_date_iso, - "end_date": end_date_iso, + "message_id": msg_id, + "message_timestamp": msg_timestamp, + "message_user_name": msg_user_name, "indexed_at": datetime.now(UTC).strftime( "%Y-%m-%d %H:%M:%S" ), - } - existing_document.chunks = chunks - existing_document.updated_at = get_current_timestamp() + }, + content=combined_document_string, + embedding=doc_embedding, + chunks=chunks, + content_hash=content_hash, + unique_identifier_hash=unique_identifier_hash, + updated_at=get_current_timestamp(), + ) - documents_indexed += 1 + session.add(document) + documents_indexed += 1 + + # Batch commit every 10 documents + if documents_indexed % 10 == 0: logger.info( - f"Successfully updated Discord channel {guild_name}#{channel_name}" + f"Committing batch: {documents_indexed} Discord messages processed so far" ) - continue + await session.commit() - # Document doesn't exist - create new one - # Get user's long context LLM - user_llm = await get_user_long_context_llm( - session, user_id, search_space_id - ) - if not user_llm: - logger.error( - f"No long context LLM configured for user {user_id}" - ) - skipped_channels.append( - f"{guild_name}#{channel_name} (no LLM configured)" - ) - documents_skipped += 1 - continue - - # Generate summary with metadata - document_metadata = { - "guild_name": guild_name, - "channel_name": channel_name, - "message_count": len(formatted_messages), - "document_type": "Discord Channel Messages", - "connector_type": "Discord", - } - ( - summary_content, - summary_embedding, - ) = await generate_document_summary( - combined_document_string, user_llm, document_metadata - ) - - # Chunks from channel content - chunks = await create_document_chunks(channel_content) - - # Create and store new document - document = Document( - search_space_id=search_space_id, - title=f"Discord - {guild_name}#{channel_name}", - document_type=DocumentType.DISCORD_CONNECTOR, - document_metadata={ - "guild_name": guild_name, - "guild_id": guild_id, - "channel_name": channel_name, - "channel_id": channel_id, - "message_count": len(formatted_messages), - "start_date": start_date_iso, - "end_date": end_date_iso, - "indexed_at": datetime.now(UTC).strftime( - "%Y-%m-%d %H:%M:%S" - ), - }, - content=summary_content, - content_hash=content_hash, - unique_identifier_hash=unique_identifier_hash, - embedding=summary_embedding, - chunks=chunks, - updated_at=get_current_timestamp(), - ) - - session.add(document) - documents_indexed += 1 logger.info( - f"Successfully indexed new channel {guild_name}#{channel_name} with {len(formatted_messages)} messages" + f"Successfully indexed channel {guild_name}#{channel_name} with {len(formatted_messages)} messages" ) - # Batch commit every 10 documents - if documents_indexed % 10 == 0: - logger.info( - f"Committing batch: {documents_indexed} Discord channels processed so far" - ) - await session.commit() - except Exception as e: logger.error( f"Error processing guild {guild_name}: {e!s}", exc_info=True @@ -553,7 +499,7 @@ async def index_discord_messages( # Final commit for any remaining documents not yet committed in batches logger.info( - f"Final commit: Total {documents_indexed} Discord channels processed" + f"Final commit: Total {documents_indexed} Discord messages processed" ) await session.commit() @@ -561,18 +507,18 @@ async def index_discord_messages( result_message = None if skipped_channels: result_message = ( - f"Processed {documents_indexed} channels. Skipped {len(skipped_channels)} channels: " + f"Processed {documents_indexed} messages. Skipped {len(skipped_channels)} channels: " + ", ".join(skipped_channels) ) else: - result_message = f"Processed {documents_indexed} channels." + result_message = f"Processed {documents_indexed} messages." # Log success await task_logger.log_task_success( log_entry, f"Successfully completed Discord indexing for connector {connector_id}", { - "channels_processed": documents_indexed, + "messages_processed": documents_indexed, "documents_indexed": documents_indexed, "documents_skipped": documents_skipped, "skipped_channels_count": len(skipped_channels), @@ -582,7 +528,7 @@ async def index_discord_messages( ) logger.info( - f"Discord indexing completed: {documents_indexed} new channels, {documents_skipped} skipped" + f"Discord indexing completed: {documents_indexed} new messages, {documents_skipped} skipped" ) return documents_indexed, result_message diff --git a/surfsense_web/app/dashboard/layout.tsx b/surfsense_web/app/dashboard/layout.tsx index 8763a622f..71cd6275f 100644 --- a/surfsense_web/app/dashboard/layout.tsx +++ b/surfsense_web/app/dashboard/layout.tsx @@ -2,7 +2,6 @@ import { Loader2 } from "lucide-react"; import { useEffect, useState } from "react"; -import { AnnouncementBanner } from "@/components/announcement-banner"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { getBearerToken, redirectToLogin } from "@/lib/auth-utils"; @@ -43,7 +42,6 @@ export default function DashboardLayout({ children }: DashboardLayoutProps) { return (
-
{children}
); diff --git a/surfsense_web/atoms/announcement.atom.ts b/surfsense_web/atoms/announcement.atom.ts deleted file mode 100644 index 31e032978..000000000 --- a/surfsense_web/atoms/announcement.atom.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { atomWithStorage } from "jotai/utils"; - -// Atom to track whether the announcement banner has been dismissed -// Persists to localStorage automatically -export const announcementDismissedAtom = atomWithStorage("surfsense_announcement_dismissed", false); diff --git a/surfsense_web/components/announcement-banner.tsx b/surfsense_web/components/announcement-banner.tsx deleted file mode 100644 index 537aa6da7..000000000 --- a/surfsense_web/components/announcement-banner.tsx +++ /dev/null @@ -1,47 +0,0 @@ -"use client"; - -import { useAtom } from "jotai"; -import { ExternalLink, Info, X } from "lucide-react"; -import { announcementDismissedAtom } from "@/atoms/announcement.atom"; -import { Button } from "@/components/ui/button"; - -export function AnnouncementBanner() { - const [isDismissed, setIsDismissed] = useAtom(announcementDismissedAtom); - - const handleDismiss = () => { - setIsDismissed(true); - }; - - if (isDismissed) return null; - - return ( -
-
-
- -

- SurfSense is a work in progress.{" "} - - Report issues on GitHub - - -

- -
-
-
- ); -} From 5d363b8a60044a4c4bd62323520ef6bde4345ec3 Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Tue, 6 Jan 2026 13:20:22 +0530 Subject: [PATCH 23/75] feat: implement Confluence OAuth integration and connector routes - Added support for Confluence OAuth with new environment variables for client ID, client secret, and redirect URI. - Implemented Confluence connector routes for OAuth flow, including authorization and callback handling. - Enhanced Confluence connector to support both OAuth 2.0 and legacy API token authentication methods. - Updated Confluence indexing logic to utilize OAuth credentials with auto-refresh capabilities. - Removed outdated Confluence UI components and adjusted frontend logic to reflect the new integration. --- surfsense_backend/app/config/__init__.py | 7 +- .../app/connectors/confluence_history.py | 488 ++++++++++++++++++ surfsense_backend/app/routes/__init__.py | 2 + .../routes/confluence_add_connector_route.py | 473 +++++++++++++++++ .../app/routes/jira_add_connector_route.py | 14 +- .../connector_indexers/confluence_indexer.py | 64 ++- surfsense_backend/app/utils/validators.py | 21 +- .../components/confluence-connect-form.tsx | 451 ---------------- .../connector-popup/connect-forms/index.tsx | 3 - .../components/confluence-config.tsx | 51 +- .../views/connector-connect-view.tsx | 1 - .../constants/connector-constants.ts | 13 +- 12 files changed, 1071 insertions(+), 517 deletions(-) create mode 100644 surfsense_backend/app/connectors/confluence_history.py create mode 100644 surfsense_backend/app/routes/confluence_add_connector_route.py delete mode 100644 surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/confluence-connect-form.tsx diff --git a/surfsense_backend/app/config/__init__.py b/surfsense_backend/app/config/__init__.py index 4abdf915a..f227f3131 100644 --- a/surfsense_backend/app/config/__init__.py +++ b/surfsense_backend/app/config/__init__.py @@ -95,10 +95,11 @@ class Config: NOTION_CLIENT_SECRET = os.getenv("NOTION_CLIENT_SECRET") NOTION_REDIRECT_URI = os.getenv("NOTION_REDIRECT_URI") - # Jira OAuth - JIRA_CLIENT_ID = os.getenv("JIRA_CLIENT_ID") - JIRA_CLIENT_SECRET = os.getenv("JIRA_CLIENT_SECRET") + # Atlassian OAuth (shared for Jira and Confluence) + ATLASSIAN_CLIENT_ID = os.getenv("ATLASSIAN_CLIENT_ID") + ATLASSIAN_CLIENT_SECRET = os.getenv("ATLASSIAN_CLIENT_SECRET") JIRA_REDIRECT_URI = os.getenv("JIRA_REDIRECT_URI") + CONFLUENCE_REDIRECT_URI = os.getenv("CONFLUENCE_REDIRECT_URI") # Linear OAuth LINEAR_CLIENT_ID = os.getenv("LINEAR_CLIENT_ID") diff --git a/surfsense_backend/app/connectors/confluence_history.py b/surfsense_backend/app/connectors/confluence_history.py new file mode 100644 index 000000000..be59e7c12 --- /dev/null +++ b/surfsense_backend/app/connectors/confluence_history.py @@ -0,0 +1,488 @@ +""" +Confluence OAuth Connector. + +Handles OAuth-based authentication and token refresh for Confluence API access. +""" + +import logging +from typing import Any + +import httpx +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.future import select + +from app.config import config +from app.db import SearchSourceConnector +from app.routes.confluence_add_connector_route import refresh_confluence_token +from app.schemas.atlassian_auth_credentials import AtlassianAuthCredentialsBase +from app.utils.oauth_security import TokenEncryption + +logger = logging.getLogger(__name__) + + +class ConfluenceHistoryConnector: + """ + Confluence connector with OAuth support and automatic token refresh. + + This connector uses OAuth 2.0 access tokens to authenticate with the + Confluence API. It automatically refreshes expired tokens when needed. + """ + + def __init__( + self, + session: AsyncSession, + connector_id: int, + credentials: AtlassianAuthCredentialsBase | None = None, + ): + """ + Initialize the ConfluenceHistoryConnector with auto-refresh capability. + + Args: + session: Database session for updating connector + connector_id: Connector ID for direct updates + credentials: Confluence OAuth credentials (optional, will be loaded from DB if not provided) + """ + self._session = session + self._connector_id = connector_id + self._credentials = credentials + self._cloud_id: str | None = None + self._base_url: str | None = None + self._http_client: httpx.AsyncClient | None = None + + async def _get_valid_token(self) -> str: + """ + Get valid Confluence access token, refreshing if needed. + + Returns: + Valid access token + + Raises: + ValueError: If credentials are missing or invalid + Exception: If token refresh fails + """ + # Load credentials from DB if not provided + if self._credentials is None: + result = await self._session.execute( + select(SearchSourceConnector).filter( + SearchSourceConnector.id == self._connector_id + ) + ) + connector = result.scalars().first() + + if not connector: + raise ValueError(f"Connector {self._connector_id} not found") + + config_data = connector.config.copy() + + # Decrypt credentials if they are encrypted + token_encrypted = config_data.get("_token_encrypted", False) + if token_encrypted and config.SECRET_KEY: + try: + token_encryption = TokenEncryption(config.SECRET_KEY) + + # Decrypt sensitive fields + if config_data.get("access_token"): + config_data["access_token"] = token_encryption.decrypt_token( + config_data["access_token"] + ) + if config_data.get("refresh_token"): + config_data["refresh_token"] = token_encryption.decrypt_token( + config_data["refresh_token"] + ) + + logger.info( + f"Decrypted Confluence credentials for connector {self._connector_id}" + ) + except Exception as e: + logger.error( + f"Failed to decrypt Confluence credentials for connector {self._connector_id}: {e!s}" + ) + raise ValueError( + f"Failed to decrypt Confluence credentials: {e!s}" + ) from e + + try: + self._credentials = AtlassianAuthCredentialsBase.from_dict(config_data) + # Store cloud_id and base_url for API calls (with backward compatibility for site_url) + self._cloud_id = config_data.get("cloud_id") + self._base_url = config_data.get("base_url") or config_data.get("site_url") + except Exception as e: + raise ValueError(f"Invalid Confluence credentials: {e!s}") from e + + # Check if token is expired and refreshable + if self._credentials.is_expired and self._credentials.is_refreshable: + try: + logger.info( + f"Confluence token expired for connector {self._connector_id}, refreshing..." + ) + + # Get connector for refresh + result = await self._session.execute( + select(SearchSourceConnector).filter( + SearchSourceConnector.id == self._connector_id + ) + ) + connector = result.scalars().first() + + if not connector: + raise RuntimeError( + f"Connector {self._connector_id} not found; cannot refresh token." + ) + + # Refresh token + connector = await refresh_confluence_token(self._session, connector) + + # Reload credentials after refresh + config_data = connector.config.copy() + token_encrypted = config_data.get("_token_encrypted", False) + if token_encrypted and config.SECRET_KEY: + token_encryption = TokenEncryption(config.SECRET_KEY) + if config_data.get("access_token"): + config_data["access_token"] = token_encryption.decrypt_token( + config_data["access_token"] + ) + if config_data.get("refresh_token"): + config_data["refresh_token"] = token_encryption.decrypt_token( + config_data["refresh_token"] + ) + + self._credentials = AtlassianAuthCredentialsBase.from_dict(config_data) + self._cloud_id = config_data.get("cloud_id") + # Handle backward compatibility: check both base_url and site_url + self._base_url = config_data.get("base_url") or config_data.get("site_url") + + # Invalidate cached client so it's recreated with new token + if self._http_client: + await self._http_client.aclose() + self._http_client = None + + logger.info( + f"Successfully refreshed Confluence token for connector {self._connector_id}" + ) + except Exception as e: + logger.error( + f"Failed to refresh Confluence token for connector {self._connector_id}: {e!s}" + ) + raise Exception( + f"Failed to refresh Confluence OAuth credentials: {e!s}" + ) from e + + return self._credentials.access_token + + async def _get_client(self) -> httpx.AsyncClient: + """ + Get or create HTTP client with valid token. + + Returns: + httpx.AsyncClient instance + """ + if self._http_client is None: + self._http_client = httpx.AsyncClient(timeout=30.0) + return self._http_client + + async def _get_base_url(self) -> str: + """ + Get the base URL for Confluence API calls. + + Returns: + Base URL string + """ + if not self._cloud_id: + raise ValueError("Cloud ID not available. Cannot construct API URL.") + + # Use the Atlassian API format: https://api.atlassian.com/ex/confluence/{cloudid} + return f"https://api.atlassian.com/ex/confluence/{self._cloud_id}" + + async def _make_api_request( + self, endpoint: str, params: dict[str, Any] | None = None + ) -> dict[str, Any]: + """ + Make a request to the Confluence API. + + Args: + endpoint: API endpoint (without base URL) + params: Query parameters for the request (optional) + + Returns: + Response data from the API + + Raises: + ValueError: If credentials have not been set + Exception: If the API request fails + """ + token = await self._get_valid_token() + base_url = await self._get_base_url() + client = await self._get_client() + + url = f"{base_url}/wiki/api/v2/{endpoint}" + headers = { + "Content-Type": "application/json", + "Authorization": f"Bearer {token}", + "Accept": "application/json", + } + + try: + response = await client.get(url, headers=headers, params=params) + response.raise_for_status() + return response.json() + except httpx.HTTPStatusError as e: + # Enhanced error logging to see the actual error + error_detail = { + "status_code": e.response.status_code, + "url": str(e.request.url), + "response_text": e.response.text, + "headers": dict(e.response.headers), + } + logger.error(f"Confluence API HTTP error: {error_detail}") + raise Exception( + f"Confluence API request failed (HTTP {e.response.status_code}): {e.response.text}" + ) from e + except httpx.RequestError as e: + logger.error(f"Confluence API request error: {e!s}", exc_info=True) + raise Exception(f"Confluence API request failed: {e!s}") from e + + async def get_all_spaces(self) -> list[dict[str, Any]]: + """ + Fetch all spaces from Confluence. + + Returns: + List of space objects + + Raises: + ValueError: If credentials have not been set + Exception: If the API request fails + """ + params = { + "limit": 100, + } + + all_spaces = [] + cursor = None + + while True: + if cursor: + params["cursor"] = cursor + + result = await self._make_api_request("spaces", params) + + if not isinstance(result, dict) or "results" not in result: + raise Exception("Invalid response from Confluence API") + + spaces = result["results"] + all_spaces.extend(spaces) + + # Check if there are more spaces to fetch + links = result.get("_links", {}) + if "next" not in links: + break + + # Extract cursor from next link if available + next_link = links["next"] + if "cursor=" in next_link: + cursor = next_link.split("cursor=")[1].split("&")[0] + else: + break + + return all_spaces + + async def get_pages_in_space( + self, space_id: str, include_body: bool = True + ) -> list[dict[str, Any]]: + """ + Fetch all pages in a specific space. + + Args: + space_id: The ID of the space to fetch pages from + include_body: Whether to include page body content + + Returns: + List of page objects + + Raises: + ValueError: If credentials have not been set + Exception: If the API request fails + """ + params = { + "limit": 100, + } + + if include_body: + params["body-format"] = "storage" + + all_pages = [] + cursor = None + + while True: + if cursor: + params["cursor"] = cursor + + result = await self._make_api_request(f"spaces/{space_id}/pages", params) + + if not isinstance(result, dict) or "results" not in result: + raise Exception("Invalid response from Confluence API") + + pages = result["results"] + all_pages.extend(pages) + + # Check if there are more pages to fetch + links = result.get("_links", {}) + if "next" not in links: + break + + # Extract cursor from next link if available + next_link = links["next"] + if "cursor=" in next_link: + cursor = next_link.split("cursor=")[1].split("&")[0] + else: + break + + return all_pages + + async def get_page_comments(self, page_id: str) -> list[dict[str, Any]]: + """ + Fetch all comments for a specific page (both footer and inline comments). + + Args: + page_id: The ID of the page to fetch comments from + + Returns: + List of comment objects + + Raises: + ValueError: If credentials have not been set + Exception: If the API request fails + """ + all_comments = [] + + # Get footer comments + footer_comments = await self._get_comments_for_page(page_id, "footer-comments") + all_comments.extend(footer_comments) + + # Get inline comments + inline_comments = await self._get_comments_for_page(page_id, "inline-comments") + all_comments.extend(inline_comments) + + return all_comments + + async def _get_comments_for_page( + self, page_id: str, comment_type: str + ) -> list[dict[str, Any]]: + """ + Helper method to fetch comments of a specific type for a page. + + Args: + page_id: The ID of the page + comment_type: Type of comments ('footer-comments' or 'inline-comments') + + Returns: + List of comment objects + """ + params = { + "limit": 100, + "body-format": "storage", + } + + all_comments = [] + cursor = None + + while True: + if cursor: + params["cursor"] = cursor + + result = await self._make_api_request(f"pages/{page_id}/{comment_type}", params) + + if not isinstance(result, dict) or "results" not in result: + break # No comments or invalid response + + comments = result["results"] + all_comments.extend(comments) + + # Check if there are more comments to fetch + links = result.get("_links", {}) + if "next" not in links: + break + + # Extract cursor from next link if available + next_link = links["next"] + if "cursor=" in next_link: + cursor = next_link.split("cursor=")[1].split("&")[0] + else: + break + + return all_comments + + async def get_pages_by_date_range( + self, + start_date: str, + end_date: str, + space_ids: list[str] | None = None, + include_comments: bool = True, + ) -> tuple[list[dict[str, Any]], str | None]: + """ + Fetch pages within a date range, optionally filtered by spaces. + + Args: + start_date: Start date in YYYY-MM-DD format + end_date: End date in YYYY-MM-DD format (inclusive) + space_ids: Optional list of space IDs to filter pages + include_comments: Whether to include comments for each page + + Returns: + Tuple containing (pages list with comments, error message or None) + """ + try: + all_pages = [] + + if space_ids: + # Fetch pages from specific spaces + for space_id in space_ids: + pages = await self.get_pages_in_space(space_id, include_body=True) + all_pages.extend(pages) + else: + # Fetch all pages (this might be expensive for large instances) + params = { + "limit": 100, + "body-format": "storage", + } + + cursor = None + while True: + if cursor: + params["cursor"] = cursor + + result = await self._make_api_request("pages", params) + if not isinstance(result, dict) or "results" not in result: + break + + pages = result["results"] + all_pages.extend(pages) + + links = result.get("_links", {}) + if "next" not in links: + break + + next_link = links["next"] + if "cursor=" in next_link: + cursor = next_link.split("cursor=")[1].split("&")[0] + else: + break + + return all_pages, None + + except Exception as e: + return [], f"Error fetching pages: {e!s}" + + async def close(self): + """Close the HTTP client connection.""" + if self._http_client: + await self._http_client.aclose() + self._http_client = None + + async def __aenter__(self): + """Async context manager entry.""" + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + """Async context manager exit.""" + await self.close() + diff --git a/surfsense_backend/app/routes/__init__.py b/surfsense_backend/app/routes/__init__.py index b7c4b2a95..5b4e24d8f 100644 --- a/surfsense_backend/app/routes/__init__.py +++ b/surfsense_backend/app/routes/__init__.py @@ -17,6 +17,7 @@ from .google_gmail_add_connector_route import ( router as google_gmail_add_connector_router, ) from .jira_add_connector_route import router as jira_add_connector_router +from .confluence_add_connector_route import router as confluence_add_connector_router from .linear_add_connector_route import router as linear_add_connector_router from .logs_routes import router as logs_router from .luma_add_connector_route import router as luma_add_connector_router @@ -50,6 +51,7 @@ router.include_router(notion_add_connector_router) router.include_router(slack_add_connector_router) router.include_router(discord_add_connector_router) router.include_router(jira_add_connector_router) +router.include_router(confluence_add_connector_router) router.include_router(new_llm_config_router) # LLM configs with prompt configuration router.include_router(logs_router) router.include_router(circleback_webhook_router) # Circleback meeting webhooks diff --git a/surfsense_backend/app/routes/confluence_add_connector_route.py b/surfsense_backend/app/routes/confluence_add_connector_route.py new file mode 100644 index 000000000..ee6556543 --- /dev/null +++ b/surfsense_backend/app/routes/confluence_add_connector_route.py @@ -0,0 +1,473 @@ +""" +Confluence Connector OAuth Routes. + +Handles OAuth 2.0 authentication flow for Confluence connector. +""" + +import logging +from datetime import UTC, datetime, timedelta +from uuid import UUID + +import httpx +from fastapi import APIRouter, Depends, HTTPException, Request +from fastapi.responses import RedirectResponse +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, + SearchSourceConnectorType, + User, + get_async_session, +) +from app.schemas.atlassian_auth_credentials import AtlassianAuthCredentialsBase +from app.users import current_active_user +from app.utils.oauth_security import OAuthStateManager, TokenEncryption + +logger = logging.getLogger(__name__) + +router = APIRouter() + +# Atlassian OAuth endpoints +AUTHORIZATION_URL = "https://auth.atlassian.com/authorize" +TOKEN_URL = "https://auth.atlassian.com/oauth/token" +RESOURCES_URL = "https://api.atlassian.com/oauth/token/accessible-resources" + +# OAuth scopes for Confluence +SCOPES = [ + "read:confluence-content.all", + "read:confluence-space.summary", + "read:confluence-user", + "read:space:confluence", + "read:page:confluence", + "read:comment:confluence", + "offline_access", # Required for refresh tokens +] + +# Initialize security utilities +_state_manager = None +_token_encryption = None + + +def get_state_manager() -> OAuthStateManager: + """Get or create OAuth state manager instance.""" + global _state_manager + if _state_manager is None: + if not config.SECRET_KEY: + raise ValueError("SECRET_KEY must be set for OAuth security") + _state_manager = OAuthStateManager(config.SECRET_KEY) + return _state_manager + + +def get_token_encryption() -> TokenEncryption: + """Get or create token encryption instance.""" + global _token_encryption + if _token_encryption is None: + if not config.SECRET_KEY: + raise ValueError("SECRET_KEY must be set for token encryption") + _token_encryption = TokenEncryption(config.SECRET_KEY) + return _token_encryption + + +@router.get("/auth/confluence/connector/add") +async def connect_confluence(space_id: int, user: User = Depends(current_active_user)): + """ + Initiate Confluence OAuth flow. + + Args: + space_id: The search space ID + user: Current authenticated user + + Returns: + Authorization URL for redirect + """ + try: + if not space_id: + raise HTTPException(status_code=400, detail="space_id is required") + + if not config.ATLASSIAN_CLIENT_ID: + raise HTTPException(status_code=500, detail="Atlassian OAuth not configured.") + + if not config.SECRET_KEY: + raise HTTPException( + status_code=500, detail="SECRET_KEY not configured for OAuth security." + ) + + # Generate secure state parameter with HMAC signature + state_manager = get_state_manager() + state_encoded = state_manager.generate_secure_state(space_id, user.id) + + # Build authorization URL + from urllib.parse import urlencode + + auth_params = { + "audience": "api.atlassian.com", + "client_id": config.ATLASSIAN_CLIENT_ID, + "scope": " ".join(SCOPES), + "redirect_uri": config.CONFLUENCE_REDIRECT_URI, + "state": state_encoded, + "response_type": "code", + "prompt": "consent", + } + + auth_url = f"{AUTHORIZATION_URL}?{urlencode(auth_params)}" + + logger.info(f"Generated Confluence OAuth URL for user {user.id}, space {space_id}") + return {"auth_url": auth_url} + + except Exception as e: + logger.error(f"Failed to initiate Confluence OAuth: {e!s}", exc_info=True) + raise HTTPException( + status_code=500, detail=f"Failed to initiate Confluence OAuth: {e!s}" + ) from e + + +@router.get("/auth/confluence/connector/callback") +async def confluence_callback( + request: Request, + code: str | None = None, + error: str | None = None, + state: str | None = None, + session: AsyncSession = Depends(get_async_session), +): + """ + Handle Confluence OAuth callback. + + Args: + request: FastAPI request object + code: Authorization code from Atlassian (if user granted access) + error: Error code from Atlassian (if user denied access or error occurred) + state: State parameter containing user/space info + session: Database session + + Returns: + Redirect response to frontend + """ + try: + # Handle OAuth errors (e.g., user denied access) + if error: + logger.warning(f"Confluence OAuth error: {error}") + # Try to decode state to get space_id for redirect, but don't fail if it's invalid + space_id = None + if state: + try: + state_manager = get_state_manager() + data = state_manager.validate_state(state) + space_id = data.get("space_id") + except Exception: + # If state is invalid, we'll redirect without space_id + logger.warning("Failed to validate state in error handler") + + # Redirect to frontend with error parameter + if space_id: + return RedirectResponse( + url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&error=confluence_oauth_denied" + ) + else: + return RedirectResponse( + url=f"{config.NEXT_FRONTEND_URL}/dashboard?error=confluence_oauth_denied" + ) + + # Validate required parameters for successful flow + if not code: + raise HTTPException(status_code=400, detail="Missing authorization code") + if not state: + raise HTTPException(status_code=400, detail="Missing state parameter") + + # Validate and decode state with signature verification + state_manager = get_state_manager() + try: + data = state_manager.validate_state(state) + except HTTPException: + raise + except Exception as e: + raise HTTPException( + status_code=400, detail=f"Invalid state parameter: {e!s}" + ) from e + + user_id = UUID(data["user_id"]) + space_id = data["space_id"] + + # Validate redirect URI (security: ensure it matches configured value) + if not config.CONFLUENCE_REDIRECT_URI: + raise HTTPException( + status_code=500, detail="CONFLUENCE_REDIRECT_URI not configured" + ) + + # Exchange authorization code for access token + token_data = { + "grant_type": "authorization_code", + "client_id": config.ATLASSIAN_CLIENT_ID, + "client_secret": config.ATLASSIAN_CLIENT_SECRET, + "code": code, + "redirect_uri": config.CONFLUENCE_REDIRECT_URI, + } + + async with httpx.AsyncClient() as client: + token_response = await client.post( + TOKEN_URL, + json=token_data, + headers={"Content-Type": "application/json"}, + timeout=30.0, + ) + + if token_response.status_code != 200: + error_detail = token_response.text + try: + error_json = token_response.json() + error_detail = error_json.get("error_description", error_json.get("error", error_detail)) + except Exception: + pass + raise HTTPException( + status_code=400, detail=f"Token exchange failed: {error_detail}" + ) + + token_json = token_response.json() + + access_token = token_json.get("access_token") + refresh_token = token_json.get("refresh_token") + if not access_token: + raise HTTPException( + status_code=400, detail="No access token received from Atlassian" + ) + + # Get accessible resources to find Confluence cloud ID and site URL + async with httpx.AsyncClient() as client: + resources_response = await client.get( + RESOURCES_URL, + headers={"Authorization": f"Bearer {access_token}"}, + timeout=30.0, + ) + + cloud_id = None + site_url = None + if resources_response.status_code == 200: + resources = resources_response.json() + # Find Confluence resource + for resource in resources: + if resource.get("id") and resource.get("name"): + cloud_id = resource.get("id") + site_url = resource.get("url") + break + + if not cloud_id: + logger.warning("Could not determine Confluence cloud ID from accessible resources") + + # Calculate expiration time (UTC, tz-aware) + expires_at = None + expires_in = token_json.get("expires_in") + if expires_in: + now_utc = datetime.now(UTC) + expires_at = now_utc + timedelta(seconds=int(expires_in)) + + # Encrypt sensitive tokens before storing + token_encryption = get_token_encryption() + + # Store the encrypted tokens and metadata in connector config + connector_config = { + "access_token": token_encryption.encrypt_token(access_token), + "refresh_token": token_encryption.encrypt_token(refresh_token) + if refresh_token + else None, + "token_type": token_json.get("token_type", "Bearer"), + "expires_in": expires_in, + "expires_at": expires_at.isoformat() if expires_at else None, + "scope": token_json.get("scope"), + "cloud_id": cloud_id, + "base_url": site_url, # Store as base_url to match shared schema + # Mark that tokens are encrypted for backward compatibility + "_token_encrypted": True, + } + + # Check if connector already exists for this search space and user + existing_connector_result = await session.execute( + select(SearchSourceConnector).filter( + SearchSourceConnector.search_space_id == space_id, + SearchSourceConnector.user_id == user_id, + SearchSourceConnector.connector_type + == SearchSourceConnectorType.CONFLUENCE_CONNECTOR, + ) + ) + existing_connector = existing_connector_result.scalars().first() + + if existing_connector: + # Update existing connector + existing_connector.config = connector_config + existing_connector.name = "Confluence Connector" + existing_connector.is_indexable = True + logger.info( + f"Updated existing Confluence connector for user {user_id} in space {space_id}" + ) + else: + # Create new connector + new_connector = SearchSourceConnector( + name="Confluence Connector", + connector_type=SearchSourceConnectorType.CONFLUENCE_CONNECTOR, + is_indexable=True, + config=connector_config, + search_space_id=space_id, + user_id=user_id, + ) + session.add(new_connector) + logger.info( + f"Created new Confluence connector for user {user_id} in space {space_id}" + ) + + try: + await session.commit() + logger.info(f"Successfully saved Confluence connector for user {user_id}") + + # Redirect to the frontend with success params + return RedirectResponse( + url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&success=true&connector=confluence-connector" + ) + + except ValidationError as e: + await session.rollback() + raise HTTPException( + status_code=422, detail=f"Validation error: {e!s}" + ) from e + except IntegrityError as e: + await session.rollback() + raise HTTPException( + status_code=409, + detail=f"Integrity error: A connector with this type already exists. {e!s}", + ) from e + except Exception as e: + logger.error(f"Failed to create search source connector: {e!s}") + await session.rollback() + raise HTTPException( + status_code=500, + detail=f"Failed to create search source connector: {e!s}", + ) from e + + except HTTPException: + raise + except Exception as e: + logger.error(f"Failed to complete Confluence OAuth: {e!s}", exc_info=True) + raise HTTPException( + status_code=500, detail=f"Failed to complete Confluence OAuth: {e!s}" + ) from e + + +async def refresh_confluence_token( + session: AsyncSession, connector: SearchSourceConnector +) -> SearchSourceConnector: + """ + Refresh the Confluence access token for a connector. + + Args: + session: Database session + connector: Confluence connector to refresh + + Returns: + Updated connector object + """ + try: + logger.info(f"Refreshing Confluence token for connector {connector.id}") + + credentials = AtlassianAuthCredentialsBase.from_dict(connector.config) + + # Decrypt tokens if they are encrypted + token_encryption = get_token_encryption() + is_encrypted = connector.config.get("_token_encrypted", False) + + refresh_token = credentials.refresh_token + if is_encrypted and refresh_token: + try: + refresh_token = token_encryption.decrypt_token(refresh_token) + except Exception as e: + logger.error(f"Failed to decrypt refresh token: {e!s}") + raise HTTPException( + status_code=500, detail="Failed to decrypt stored refresh token" + ) from e + + if not refresh_token: + raise HTTPException( + status_code=400, + detail="No refresh token available. Please re-authenticate.", + ) + + # Prepare token refresh data + refresh_data = { + "grant_type": "refresh_token", + "client_id": config.ATLASSIAN_CLIENT_ID, + "client_secret": config.ATLASSIAN_CLIENT_SECRET, + "refresh_token": refresh_token, + } + + async with httpx.AsyncClient() as client: + token_response = await client.post( + TOKEN_URL, + json=refresh_data, + headers={"Content-Type": "application/json"}, + timeout=30.0, + ) + + if token_response.status_code != 200: + error_detail = token_response.text + try: + error_json = token_response.json() + error_detail = error_json.get("error_description", error_json.get("error", error_detail)) + except Exception: + pass + raise HTTPException( + status_code=400, detail=f"Token refresh failed: {error_detail}" + ) + + token_json = token_response.json() + + # Calculate expiration time (UTC, tz-aware) + expires_at = None + expires_in = token_json.get("expires_in") + if expires_in: + now_utc = datetime.now(UTC) + expires_at = now_utc + timedelta(seconds=int(expires_in)) + + # Encrypt new tokens before storing + access_token = token_json.get("access_token") + new_refresh_token = token_json.get("refresh_token") + + if not access_token: + raise HTTPException( + status_code=400, detail="No access token received from Confluence refresh" + ) + + # Update credentials object with encrypted tokens + credentials.access_token = token_encryption.encrypt_token(access_token) + if new_refresh_token: + credentials.refresh_token = token_encryption.encrypt_token( + new_refresh_token + ) + credentials.expires_in = expires_in + credentials.expires_at = expires_at + credentials.scope = token_json.get("scope") + + # Preserve cloud_id and base_url (with backward compatibility for site_url) + if not credentials.cloud_id: + credentials.cloud_id = connector.config.get("cloud_id") + if not credentials.base_url: + # Check both base_url and site_url for backward compatibility + credentials.base_url = connector.config.get("base_url") or connector.config.get("site_url") + + # Update connector config with encrypted tokens + credentials_dict = credentials.to_dict() + credentials_dict["_token_encrypted"] = True + connector.config = credentials_dict + await session.commit() + await session.refresh(connector) + + logger.info(f"Successfully refreshed Confluence token for connector {connector.id}") + + return connector + except HTTPException: + raise + except Exception as e: + logger.error(f"Failed to refresh Confluence token: {e!s}", exc_info=True) + raise HTTPException( + status_code=500, detail=f"Failed to refresh Confluence token: {e!s}" + ) from e + diff --git a/surfsense_backend/app/routes/jira_add_connector_route.py b/surfsense_backend/app/routes/jira_add_connector_route.py index 302a118db..c7ad835ba 100644 --- a/surfsense_backend/app/routes/jira_add_connector_route.py +++ b/surfsense_backend/app/routes/jira_add_connector_route.py @@ -86,8 +86,8 @@ async def connect_jira(space_id: int, user: User = Depends(current_active_user)) if not space_id: raise HTTPException(status_code=400, detail="space_id is required") - if not config.JIRA_CLIENT_ID: - raise HTTPException(status_code=500, detail="Jira OAuth not configured.") + if not config.ATLASSIAN_CLIENT_ID: + raise HTTPException(status_code=500, detail="Atlassian OAuth not configured.") if not config.SECRET_KEY: raise HTTPException( @@ -103,7 +103,7 @@ async def connect_jira(space_id: int, user: User = Depends(current_active_user)) auth_params = { "audience": "api.atlassian.com", - "client_id": config.JIRA_CLIENT_ID, + "client_id": config.ATLASSIAN_CLIENT_ID, "scope": " ".join(SCOPES), "redirect_uri": config.JIRA_REDIRECT_URI, "state": state_encoded, @@ -198,8 +198,8 @@ async def jira_callback( # Exchange authorization code for access token token_data = { "grant_type": "authorization_code", - "client_id": config.JIRA_CLIENT_ID, - "client_secret": config.JIRA_CLIENT_SECRET, + "client_id": config.ATLASSIAN_CLIENT_ID, + "client_secret": config.ATLASSIAN_CLIENT_SECRET, "code": code, "redirect_uri": config.JIRA_REDIRECT_URI, } @@ -417,8 +417,8 @@ async def refresh_jira_token( # Prepare token refresh data refresh_data = { "grant_type": "refresh_token", - "client_id": config.JIRA_CLIENT_ID, - "client_secret": config.JIRA_CLIENT_SECRET, + "client_id": config.ATLASSIAN_CLIENT_ID, + "client_secret": config.ATLASSIAN_CLIENT_SECRET, "refresh_token": refresh_token, } diff --git a/surfsense_backend/app/tasks/connector_indexers/confluence_indexer.py b/surfsense_backend/app/tasks/connector_indexers/confluence_indexer.py index d5e68fb8f..85aa10e1a 100644 --- a/surfsense_backend/app/tasks/connector_indexers/confluence_indexer.py +++ b/surfsense_backend/app/tasks/connector_indexers/confluence_indexer.py @@ -8,7 +8,7 @@ from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.ext.asyncio import AsyncSession from app.config import config -from app.connectors.confluence_connector import ConfluenceConnector +from app.connectors.confluence_history import ConfluenceHistoryConnector from app.db import Document, DocumentType, SearchSourceConnectorType from app.services.llm_service import get_user_long_context_llm from app.services.task_logging_service import TaskLoggingService @@ -83,31 +83,16 @@ async def index_confluence_pages( ) return 0, f"Connector with ID {connector_id} not found" - # Get the Confluence credentials from the connector config - confluence_email = connector.config.get("CONFLUENCE_EMAIL") - confluence_api_token = connector.config.get("CONFLUENCE_API_TOKEN") - confluence_base_url = connector.config.get("CONFLUENCE_BASE_URL") - - if not confluence_email or not confluence_api_token or not confluence_base_url: - await task_logger.log_task_failure( - log_entry, - f"Confluence credentials not found in connector config for connector {connector_id}", - "Missing Confluence credentials", - {"error_type": "MissingCredentials"}, - ) - return 0, "Confluence credentials not found in connector config" - - # Initialize Confluence client + # Initialize Confluence OAuth client await task_logger.log_task_progress( log_entry, - f"Initializing Confluence client for connector {connector_id}", + f"Initializing Confluence OAuth client for connector {connector_id}", {"stage": "client_initialization"}, ) - confluence_client = ConfluenceConnector( - base_url=confluence_base_url, - email=confluence_email, - api_token=confluence_api_token, + confluence_client: ConfluenceHistoryConnector | None = ConfluenceHistoryConnector( + session=session, + connector_id=connector_id, ) # Calculate date range @@ -127,7 +112,7 @@ async def index_confluence_pages( # Get pages within date range try: - pages, error = confluence_client.get_pages_by_date_range( + pages, error = await confluence_client.get_pages_by_date_range( start_date=start_date_str, end_date=end_date_str, include_comments=True ) @@ -153,6 +138,12 @@ async def index_confluence_pages( f"No Confluence pages found in date range {start_date_str} to {end_date_str}", {"pages_found": 0}, ) + # Close client before returning + if confluence_client: + try: + await confluence_client.close() + except Exception: + pass return 0, None else: await task_logger.log_task_failure( @@ -161,12 +152,24 @@ async def index_confluence_pages( "API Error", {"error_type": "APIError"}, ) + # Close client on error + if confluence_client: + try: + await confluence_client.close() + except Exception: + pass return 0, f"Failed to get Confluence pages: {error}" logger.info(f"Retrieved {len(pages)} pages from Confluence API") except Exception as e: logger.error(f"Error fetching Confluence pages: {e!s}", exc_info=True) + # Close client on error + if confluence_client: + try: + await confluence_client.close() + except Exception: + pass return 0, f"Error fetching Confluence pages: {e!s}" # Process and index each page @@ -418,6 +421,11 @@ async def index_confluence_pages( logger.info( f"Confluence indexing completed: {documents_indexed} new pages, {documents_skipped} skipped" ) + + # Close the client connection + if confluence_client: + await confluence_client.close() + return ( total_processed, None, @@ -425,6 +433,12 @@ async def index_confluence_pages( except SQLAlchemyError as db_error: await session.rollback() + # Close client if it exists + if confluence_client: + try: + await confluence_client.close() + except Exception: + pass await task_logger.log_task_failure( log_entry, f"Database error during Confluence indexing for connector {connector_id}", @@ -435,6 +449,12 @@ async def index_confluence_pages( return 0, f"Database error: {db_error!s}" except Exception as e: await session.rollback() + # Close client if it exists + if confluence_client: + try: + await confluence_client.close() + except Exception: + pass await task_logger.log_task_failure( log_entry, f"Failed to index Confluence pages for connector {connector_id}", diff --git a/surfsense_backend/app/utils/validators.py b/surfsense_backend/app/utils/validators.py index d1f416339..adc8f9ee7 100644 --- a/surfsense_backend/app/utils/validators.py +++ b/surfsense_backend/app/utils/validators.py @@ -545,21 +545,12 @@ def validate_connector_config( # "JIRA_BASE_URL": lambda: validate_url_field("JIRA_BASE_URL", "JIRA"), # }, # }, - "CONFLUENCE_CONNECTOR": { - "required": [ - "CONFLUENCE_BASE_URL", - "CONFLUENCE_EMAIL", - "CONFLUENCE_API_TOKEN", - ], - "validators": { - "CONFLUENCE_EMAIL": lambda: validate_email_field( - "CONFLUENCE_EMAIL", "Confluence" - ), - "CONFLUENCE_BASE_URL": lambda: validate_url_field( - "CONFLUENCE_BASE_URL", "Confluence" - ), - }, - }, + # "CONFLUENCE_CONNECTOR": { + # "required": [ + # "access_token", + # ], + # "validators": {}, + # }, "CLICKUP_CONNECTOR": {"required": ["CLICKUP_API_TOKEN"], "validators": {}}, # "GOOGLE_CALENDAR_CONNECTOR": { # "required": ["token", "refresh_token", "token_uri", "client_id", "expiry", "scopes", "client_secret"], diff --git a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/confluence-connect-form.tsx b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/confluence-connect-form.tsx deleted file mode 100644 index 83f6c6ec7..000000000 --- a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/confluence-connect-form.tsx +++ /dev/null @@ -1,451 +0,0 @@ -"use client"; - -import { zodResolver } from "@hookform/resolvers/zod"; -import { Info } from "lucide-react"; -import type { FC } from "react"; -import { useRef, useState } from "react"; -import { useForm } from "react-hook-form"; -import * as z from "zod"; -import { - Accordion, - AccordionContent, - AccordionItem, - AccordionTrigger, -} from "@/components/ui/accordion"; -import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; -import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { Switch } from "@/components/ui/switch"; -import { EnumConnectorName } from "@/contracts/enums/connector"; -import { DateRangeSelector } from "../../components/date-range-selector"; -import { getConnectorBenefits } from "../connector-benefits"; -import type { ConnectFormProps } from "../index"; - -const confluenceConnectorFormSchema = z.object({ - name: z.string().min(3, { - message: "Connector name must be at least 3 characters.", - }), - base_url: z.string().url({ message: "Please enter a valid Confluence base URL." }), - email: z.string().email({ message: "Please enter a valid email address." }), - api_token: z.string().min(10, { - message: "Confluence API Token is required and must be valid.", - }), -}); - -type ConfluenceConnectorFormValues = z.infer; - -export const ConfluenceConnectForm: FC = ({ onSubmit, isSubmitting }) => { - const isSubmittingRef = useRef(false); - const [startDate, setStartDate] = useState(undefined); - const [endDate, setEndDate] = useState(undefined); - const [periodicEnabled, setPeriodicEnabled] = useState(false); - const [frequencyMinutes, setFrequencyMinutes] = useState("1440"); - const form = useForm({ - resolver: zodResolver(confluenceConnectorFormSchema), - defaultValues: { - name: "Confluence Connector", - base_url: "", - email: "", - api_token: "", - }, - }); - - const handleSubmit = async (values: ConfluenceConnectorFormValues) => { - // Prevent multiple submissions - if (isSubmittingRef.current || isSubmitting) { - return; - } - - isSubmittingRef.current = true; - try { - await onSubmit({ - name: values.name, - connector_type: EnumConnectorName.CONFLUENCE_CONNECTOR, - config: { - CONFLUENCE_BASE_URL: values.base_url, - CONFLUENCE_EMAIL: values.email, - CONFLUENCE_API_TOKEN: values.api_token, - }, - is_indexable: true, - last_indexed_at: null, - periodic_indexing_enabled: periodicEnabled, - indexing_frequency_minutes: periodicEnabled ? parseInt(frequencyMinutes, 10) : null, - next_scheduled_at: null, - startDate, - endDate, - periodicEnabled, - frequencyMinutes, - }); - } finally { - isSubmittingRef.current = false; - } - }; - - return ( -
- - -
- API Token Required - - You'll need a Confluence API Token to use this connector. You can create one from{" "} - - Atlassian Account Settings - - -
-
- -
-
- - ( - - Connector Name - - - - - A friendly name to identify this connector. - - - - )} - /> - - ( - - Confluence Base URL - - - - - The base URL of your Confluence instance (e.g., - https://your-domain.atlassian.net). - - - - )} - /> - - ( - - Email Address - - - - - The email address associated with your Atlassian account. - - - - )} - /> - - ( - - API Token - - - - - Your Confluence API Token will be encrypted and stored securely. - - - - )} - /> - - {/* Indexing Configuration */} -
-

Indexing Configuration

- - {/* Date Range Selector */} - - - {/* Periodic Sync Config */} -
-
-
-

Enable Periodic Sync

-

- Automatically re-index at regular intervals -

-
- -
- - {periodicEnabled && ( -
-
- - -
-
- )} -
-
- - -
- - {/* What you get section */} - {getConnectorBenefits(EnumConnectorName.CONFLUENCE_CONNECTOR) && ( -
-

- What you get with Confluence integration: -

-
    - {getConnectorBenefits(EnumConnectorName.CONFLUENCE_CONNECTOR)?.map((benefit) => ( -
  • {benefit}
  • - ))} -
-
- )} - - {/* Documentation Section */} - - - - Documentation - - -
-

How it works

-

- The Confluence connector uses the Confluence REST API to fetch all pages and - comments that your account has access to within your Confluence instance. -

-
    -
  • - For follow up indexing runs, the connector retrieves pages and comments that have - been updated since the last indexing attempt. -
  • -
  • - Indexing is configured to run periodically, so updates should appear in your - search results within minutes. -
  • -
-
- -
-
-

Authorization

- - - - Read-Only Access is Sufficient - - - You only need read access for this connector to work. The API Token will only be - used to read your Confluence data. - - - -
-
-

- Step 1: Create an API Token -

-
    -
  1. Log in to your Atlassian account
  2. -
  3. - Navigate to{" "} - - https://id.atlassian.com/manage-profile/security/api-tokens - {" "} - in your browser. -
  4. -
  5. - Click Create API token -
  6. -
  7. Enter a label for your token (like "SurfSense Connector")
  8. -
  9. - Click Create -
  10. -
  11. Copy the generated token as it will only be shown once
  12. -
-
- -
-

- Step 2: Grant necessary access -

-

- The API Token will have access to all spaces and pages that your user account - can see. Make sure your account has appropriate permissions for the spaces you - want to index. -

- - - Data Privacy - - Only pages, comments, and basic metadata will be indexed. Confluence - attachments and linked files are not indexed by this connector. - - -
-
-
-
- -
-
-

Indexing

-
    -
  1. - Navigate to the Connector Dashboard and select the Confluence{" "} - Connector. -
  2. -
  3. - Enter your Confluence Instance URL (e.g., - https://yourcompany.atlassian.net) -
  4. -
  5. - Enter your Email Address associated with your Atlassian account -
  6. -
  7. - Place your API Token in the form field. -
  8. -
  9. - Click Connect to establish the connection. -
  10. -
  11. Once connected, your Confluence pages will be indexed automatically.
  12. -
- - - - What Gets Indexed - -

The Confluence connector indexes the following data:

-
    -
  • All pages from accessible spaces
  • -
  • Page content and metadata
  • -
  • Comments on pages (both footer and inline comments)
  • -
  • Page titles and descriptions
  • -
-
-
-
-
-
-
-
-
- ); -}; diff --git a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/index.tsx b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/index.tsx index cda17ddfc..86a70b5bf 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/index.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/index.tsx @@ -3,7 +3,6 @@ import { BaiduSearchApiConnectForm } from "./components/baidu-search-api-connect import { BookStackConnectForm } from "./components/bookstack-connect-form"; import { CirclebackConnectForm } from "./components/circleback-connect-form"; import { ClickUpConnectForm } from "./components/clickup-connect-form"; -import { ConfluenceConnectForm } from "./components/confluence-connect-form"; import { ElasticsearchConnectForm } from "./components/elasticsearch-connect-form"; import { GithubConnectForm } from "./components/github-connect-form"; import { LinkupApiConnectForm } from "./components/linkup-api-connect-form"; @@ -48,8 +47,6 @@ export function getConnectFormComponent(connectorType: string): ConnectFormCompo return BaiduSearchApiConnectForm; case "ELASTICSEARCH_CONNECTOR": return ElasticsearchConnectForm; - case "CONFLUENCE_CONNECTOR": - return ConfluenceConnectForm; case "BOOKSTACK_CONNECTOR": return BookStackConnectForm; case "GITHUB_CONNECTOR": diff --git a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/confluence-config.tsx b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/confluence-config.tsx index c3a233406..f757e603a 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/confluence-config.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/confluence-config.tsx @@ -1,6 +1,6 @@ "use client"; -import { KeyRound } from "lucide-react"; +import { Info, KeyRound } from "lucide-react"; import type { FC } from "react"; import { useEffect, useState } from "react"; import { Input } from "@/components/ui/input"; @@ -16,6 +16,9 @@ export const ConfluenceConfig: FC = ({ onConfigChange, onNameChange, }) => { + // Check if this is an OAuth connector (has access_token or _token_encrypted flag) + const isOAuth = !!(connector.config?.access_token || connector.config?._token_encrypted); + const [baseUrl, setBaseUrl] = useState( (connector.config?.CONFLUENCE_BASE_URL as string) || "" ); @@ -25,16 +28,18 @@ export const ConfluenceConfig: FC = ({ ); const [name, setName] = useState(connector.name || ""); - // Update values when connector changes + // Update values when connector changes (only for legacy connectors) useEffect(() => { - const url = (connector.config?.CONFLUENCE_BASE_URL as string) || ""; - const emailVal = (connector.config?.CONFLUENCE_EMAIL as string) || ""; - const token = (connector.config?.CONFLUENCE_API_TOKEN as string) || ""; - setBaseUrl(url); - setEmail(emailVal); - setApiToken(token); + if (!isOAuth) { + const url = (connector.config?.CONFLUENCE_BASE_URL as string) || ""; + const emailVal = (connector.config?.CONFLUENCE_EMAIL as string) || ""; + const token = (connector.config?.CONFLUENCE_API_TOKEN as string) || ""; + setBaseUrl(url); + setEmail(emailVal); + setApiToken(token); + } setName(connector.name || ""); - }, [connector.config, connector.name]); + }, [connector.config, connector.name, isOAuth]); const handleBaseUrlChange = (value: string) => { setBaseUrl(value); @@ -73,6 +78,34 @@ export const ConfluenceConfig: FC = ({ } }; + // For OAuth connectors, show simple info message + if (isOAuth) { + const siteUrl = (connector.config?.site_url as string) || "Unknown"; + return ( +
+ {/* OAuth Info */} +
+
+ +
+
+

Connected via OAuth

+

+ This connector is authenticated using OAuth 2.0. Your Confluence instance is: +

+

+ {siteUrl} +

+

+ To update your connection, disconnect and reconnect through the OAuth flow. +

+
+
+
+ ); + } + + // For legacy API token connectors, show the form return (
{/* Connector Name */} diff --git a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-connect-view.tsx b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-connect-view.tsx index 7b0c3e82f..22dff4322 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-connect-view.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-connect-view.tsx @@ -52,7 +52,6 @@ export const ConnectorConnectView: FC = ({ LINKUP_API: "linkup-api-connect-form", BAIDU_SEARCH_API: "baidu-search-api-connect-form", ELASTICSEARCH_CONNECTOR: "elasticsearch-connect-form", - CONFLUENCE_CONNECTOR: "confluence-connect-form", BOOKSTACK_CONNECTOR: "bookstack-connect-form", GITHUB_CONNECTOR: "github-connect-form", CLICKUP_CONNECTOR: "clickup-connect-form", diff --git a/surfsense_web/components/assistant-ui/connector-popup/constants/connector-constants.ts b/surfsense_web/components/assistant-ui/connector-popup/constants/connector-constants.ts index 0e942dd1e..4d15d0989 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/constants/connector-constants.ts +++ b/surfsense_web/components/assistant-ui/connector-popup/constants/connector-constants.ts @@ -65,6 +65,13 @@ export const OAUTH_CONNECTORS = [ connectorType: EnumConnectorName.JIRA_CONNECTOR, authEndpoint: "/api/v1/auth/jira/connector/add/", }, + { + id: "confluence-connector", + title: "Confluence", + description: "Search documentation", + connectorType: EnumConnectorName.CONFLUENCE_CONNECTOR, + authEndpoint: "/api/v1/auth/confluence/connector/add/", + }, ] as const; // Content Sources (tools that extract and import content from external sources) @@ -85,12 +92,6 @@ export const CRAWLERS = [ // Non-OAuth Connectors (redirect to old connector config pages) export const OTHER_CONNECTORS = [ - { - id: "confluence-connector", - title: "Confluence", - description: "Search documentation", - connectorType: EnumConnectorName.CONFLUENCE_CONNECTOR, - }, { id: "bookstack-connector", title: "BookStack", From 0f5bf93f687fc13f0c06094c6aa22aed0a837eda Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Tue, 6 Jan 2026 14:36:51 +0530 Subject: [PATCH 24/75] feat: implement JiraHistoryConnector for OAuth and legacy authentication - Introduced JiraHistoryConnector to handle OAuth-based authentication and automatic token refresh for Jira API access. - Refactored Jira indexing logic to utilize the new connector, simplifying credential management and enhancing token refresh capabilities. - Removed legacy token handling code from the Jira indexer, streamlining the integration process. - Ensured compatibility with both OAuth 2.0 and legacy API token methods for improved flexibility. --- .../app/connectors/jira_history.py | 320 ++++++++++++++++++ .../tasks/connector_indexers/jira_indexer.py | 155 ++------- 2 files changed, 351 insertions(+), 124 deletions(-) create mode 100644 surfsense_backend/app/connectors/jira_history.py diff --git a/surfsense_backend/app/connectors/jira_history.py b/surfsense_backend/app/connectors/jira_history.py new file mode 100644 index 000000000..3e8c69104 --- /dev/null +++ b/surfsense_backend/app/connectors/jira_history.py @@ -0,0 +1,320 @@ +""" +Jira OAuth Connector. + +Handles OAuth-based authentication and token refresh for Jira API access. +Supports both OAuth 2.0 (preferred) and legacy API token authentication. +""" + +import logging +from typing import Any + +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.future import select + +from app.config import config +from app.connectors.jira_connector import JiraConnector +from app.db import SearchSourceConnector +from app.routes.jira_add_connector_route import refresh_jira_token +from app.schemas.atlassian_auth_credentials import AtlassianAuthCredentialsBase +from app.utils.oauth_security import TokenEncryption + +logger = logging.getLogger(__name__) + + +class JiraHistoryConnector: + """ + Jira connector with OAuth support and automatic token refresh. + + This connector uses OAuth 2.0 access tokens to authenticate with the + Jira API. It automatically refreshes expired tokens when needed. + Also supports legacy API token authentication for backward compatibility. + """ + + def __init__( + self, + session: AsyncSession, + connector_id: int, + credentials: AtlassianAuthCredentialsBase | None = None, + ): + """ + Initialize the JiraHistoryConnector with auto-refresh capability. + + Args: + session: Database session for updating connector + connector_id: Connector ID for direct updates + credentials: Jira OAuth credentials (optional, will be loaded from DB if not provided) + """ + self._session = session + self._connector_id = connector_id + self._credentials = credentials + self._cloud_id: str | None = None + self._base_url: str | None = None + self._jira_client: JiraConnector | None = None + self._use_oauth = True + self._legacy_email: str | None = None + self._legacy_api_token: str | None = None + + async def _get_valid_token(self) -> str: + """ + Get valid Jira access token, refreshing if needed. + + Returns: + Valid access token + + Raises: + ValueError: If credentials are missing or invalid + Exception: If token refresh fails + """ + # Load credentials from DB if not provided + if self._credentials is None: + result = await self._session.execute( + select(SearchSourceConnector).filter( + SearchSourceConnector.id == self._connector_id + ) + ) + connector = result.scalars().first() + + if not connector: + raise ValueError(f"Connector {self._connector_id} not found") + + config_data = connector.config.copy() + + # Check if using OAuth or legacy API token + is_oauth = config_data.get("_token_encrypted", False) or config_data.get("access_token") + + if is_oauth: + # OAuth 2.0 authentication + if not config.SECRET_KEY: + raise ValueError( + "SECRET_KEY not configured but tokens are marked as encrypted" + ) + + try: + token_encryption = TokenEncryption(config.SECRET_KEY) + + # Decrypt access_token + if config_data.get("access_token"): + config_data["access_token"] = token_encryption.decrypt_token( + config_data["access_token"] + ) + logger.info( + f"Decrypted Jira access token for connector {self._connector_id}" + ) + + # Decrypt refresh_token if present + if config_data.get("refresh_token"): + config_data["refresh_token"] = token_encryption.decrypt_token( + config_data["refresh_token"] + ) + logger.info( + f"Decrypted Jira refresh token for connector {self._connector_id}" + ) + except Exception as e: + logger.error( + f"Failed to decrypt Jira credentials for connector {self._connector_id}: {e!s}" + ) + raise ValueError( + f"Failed to decrypt Jira credentials: {e!s}" + ) from e + + try: + self._credentials = AtlassianAuthCredentialsBase.from_dict(config_data) + self._cloud_id = config_data.get("cloud_id") + self._base_url = config_data.get("base_url") + self._use_oauth = True + except Exception as e: + raise ValueError(f"Invalid Jira OAuth credentials: {e!s}") from e + else: + # Legacy API token authentication + self._legacy_email = config_data.get("JIRA_EMAIL") + self._legacy_api_token = config_data.get("JIRA_API_TOKEN") + self._base_url = config_data.get("JIRA_BASE_URL") + self._use_oauth = False + + if not self._legacy_email or not self._legacy_api_token or not self._base_url: + raise ValueError("Jira credentials not found in connector config") + + # Check if token is expired and refreshable (only for OAuth) + if self._use_oauth and self._credentials.is_expired and self._credentials.is_refreshable: + try: + logger.info( + f"Jira token expired for connector {self._connector_id}, refreshing..." + ) + + # Get connector for refresh + result = await self._session.execute( + select(SearchSourceConnector).filter( + SearchSourceConnector.id == self._connector_id + ) + ) + connector = result.scalars().first() + + if not connector: + raise RuntimeError( + f"Connector {self._connector_id} not found; cannot refresh token." + ) + + # Refresh token + connector = await refresh_jira_token(self._session, connector) + + # Reload credentials after refresh + config_data = connector.config.copy() + token_encrypted = config_data.get("_token_encrypted", False) + if token_encrypted and config.SECRET_KEY: + token_encryption = TokenEncryption(config.SECRET_KEY) + if config_data.get("access_token"): + config_data["access_token"] = token_encryption.decrypt_token( + config_data["access_token"] + ) + if config_data.get("refresh_token"): + config_data["refresh_token"] = token_encryption.decrypt_token( + config_data["refresh_token"] + ) + + self._credentials = AtlassianAuthCredentialsBase.from_dict(config_data) + self._cloud_id = config_data.get("cloud_id") + self._base_url = config_data.get("base_url") + + # Invalidate cached client so it's recreated with new token + self._jira_client = None + + logger.info( + f"Successfully refreshed Jira token for connector {self._connector_id}" + ) + except Exception as e: + logger.error( + f"Failed to refresh Jira token for connector {self._connector_id}: {e!s}" + ) + raise Exception( + f"Failed to refresh Jira OAuth credentials: {e!s}" + ) from e + + if self._use_oauth: + return self._credentials.access_token + else: + # For legacy auth, return empty string (not used for token-based auth) + return "" + + async def _get_jira_client(self) -> JiraConnector: + """ + Get or create JiraConnector with valid credentials. + + Returns: + JiraConnector instance + """ + if self._jira_client is None: + if self._use_oauth: + # Ensure we have valid token (will refresh if needed) + await self._get_valid_token() + + self._jira_client = JiraConnector( + base_url=self._base_url, + access_token=self._credentials.access_token, + cloud_id=self._cloud_id, + ) + else: + # Legacy API token authentication + self._jira_client = JiraConnector( + base_url=self._base_url, + email=self._legacy_email, + api_token=self._legacy_api_token, + ) + else: + # If OAuth, refresh token if expired before returning client + if self._use_oauth: + await self._get_valid_token() + # Update client with new token if it was refreshed + if self._credentials: + self._jira_client.set_oauth_credentials( + base_url=self._base_url or "", + access_token=self._credentials.access_token, + cloud_id=self._cloud_id, + ) + + return self._jira_client + + async def get_issues_by_date_range( + self, + start_date: str, + end_date: str, + include_comments: bool = True, + project_key: str | None = None, + ) -> tuple[list[dict[str, Any]], str | None]: + """ + Fetch issues within a date range. + This method wraps JiraConnector.get_issues_by_date_range() with automatic token refresh. + + Args: + start_date: Start date in YYYY-MM-DD format + end_date: End date in YYYY-MM-DD format (inclusive) + include_comments: Whether to include comments in the response + project_key: Optional project key to filter issues + + Returns: + Tuple containing (issues list, error message or None) + """ + # Ensure token is valid (will refresh if needed) + if self._use_oauth: + await self._get_valid_token() + + # Get client with valid credentials + client = await self._get_jira_client() + + # JiraConnector methods are synchronous, so we call them directly + # Token refresh has already been handled above + return client.get_issues_by_date_range( + start_date=start_date, + end_date=end_date, + include_comments=include_comments, + project_key=project_key, + ) + + def format_issue(self, issue: dict[str, Any]) -> dict[str, Any]: + """ + Format an issue for easier consumption. + Wraps JiraConnector.format_issue(). + + Args: + issue: The issue object from Jira API + + Returns: + Formatted issue dictionary + """ + # This is a synchronous method that doesn't need token refresh + # since it just formats data that's already been fetched + if self._jira_client is None: + # Create a minimal client just for formatting (doesn't need credentials) + self._jira_client = JiraConnector() + return self._jira_client.format_issue(issue) + + def format_issue_to_markdown(self, issue: dict[str, Any]) -> str: + """ + Convert an issue to markdown format. + Wraps JiraConnector.format_issue_to_markdown(). + + Args: + issue: The issue object (either raw or formatted) + + Returns: + Markdown string representation of the issue + """ + # This is a synchronous method that doesn't need token refresh + # since it just formats data that's already been fetched + if self._jira_client is None: + # Create a minimal client just for formatting (doesn't need credentials) + self._jira_client = JiraConnector() + return self._jira_client.format_issue_to_markdown(issue) + + async def close(self): + """Close any resources (currently no-op for JiraConnector).""" + # JiraConnector doesn't maintain persistent connections, so nothing to close + self._jira_client = None + + async def __aenter__(self): + """Async context manager entry.""" + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + """Async context manager exit.""" + await self.close() + diff --git a/surfsense_backend/app/tasks/connector_indexers/jira_indexer.py b/surfsense_backend/app/tasks/connector_indexers/jira_indexer.py index cd7dabeaf..47ad0986f 100644 --- a/surfsense_backend/app/tasks/connector_indexers/jira_indexer.py +++ b/surfsense_backend/app/tasks/connector_indexers/jira_indexer.py @@ -8,7 +8,7 @@ from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.ext.asyncio import AsyncSession from app.config import config -from app.connectors.jira_connector import JiraConnector +from app.connectors.jira_history import JiraHistoryConnector from app.db import Document, DocumentType, SearchSourceConnectorType from app.services.llm_service import get_user_long_context_llm from app.services.task_logging_service import TaskLoggingService @@ -83,130 +83,21 @@ async def index_jira_issues( ) return 0, f"Connector with ID {connector_id} not found" - # Get the Jira credentials from the connector config - # Support both OAuth (preferred) and legacy API token authentication - config_data = connector.config.copy() - is_oauth = config_data.get("_token_encrypted", False) or config_data.get("access_token") + # Initialize Jira client with internal refresh capability + # Token refresh will happen automatically when needed + await task_logger.log_task_progress( + log_entry, + f"Initializing Jira client for connector {connector_id}", + {"stage": "client_initialization"}, + ) - if is_oauth: - # OAuth 2.0 authentication - from app.utils.oauth_security import TokenEncryption + logger.info(f"Initializing Jira client for connector {connector_id}") - if not config.SECRET_KEY: - await task_logger.log_task_failure( - log_entry, - f"SECRET_KEY not configured but tokens are marked as encrypted for connector {connector_id}", - "Missing SECRET_KEY for token decryption", - {"error_type": "MissingSecretKey"}, - ) - return 0, "SECRET_KEY not configured but tokens are marked as encrypted" - - try: - token_encryption = TokenEncryption(config.SECRET_KEY) - - # Decrypt access_token - if config_data.get("access_token"): - config_data["access_token"] = token_encryption.decrypt_token( - config_data["access_token"] - ) - logger.info( - f"Decrypted Jira access token for connector {connector_id}" - ) - - # Decrypt refresh_token if present - if config_data.get("refresh_token"): - config_data["refresh_token"] = token_encryption.decrypt_token( - config_data["refresh_token"] - ) - logger.info( - f"Decrypted Jira refresh token for connector {connector_id}" - ) - except Exception as e: - await task_logger.log_task_failure( - log_entry, - f"Failed to decrypt Jira tokens for connector {connector_id}: {e!s}", - "Token decryption failed", - {"error_type": "TokenDecryptionError"}, - ) - return 0, f"Failed to decrypt Jira tokens: {e!s}" - - try: - from app.schemas.atlassian_auth_credentials import AtlassianAuthCredentialsBase - credentials = AtlassianAuthCredentialsBase.from_dict(config_data) - except Exception as e: - await task_logger.log_task_failure( - log_entry, - f"Invalid Jira OAuth credentials in connector {connector_id}", - str(e), - {"error_type": "InvalidCredentials"}, - ) - return 0, f"Invalid Jira OAuth credentials: {e!s}" - - # Check if credentials are expired and refresh if needed - if credentials.is_expired: - await task_logger.log_task_progress( - log_entry, - f"Jira credentials expired for connector {connector_id}, refreshing token", - {"stage": "token_refresh"}, - ) - - from app.routes.jira_add_connector_route import refresh_jira_token - - try: - connector = await refresh_jira_token(session, connector) - # Re-fetch credentials after refresh - config_data = connector.config.copy() - if config_data.get("access_token"): - config_data["access_token"] = token_encryption.decrypt_token( - config_data["access_token"] - ) - credentials = AtlassianAuthCredentialsBase.from_dict(config_data) - except Exception as e: - await task_logger.log_task_failure( - log_entry, - f"Failed to refresh Jira token for connector {connector_id}: {e!s}", - "Token refresh failed", - {"error_type": "TokenRefreshError"}, - ) - return 0, f"Failed to refresh Jira token: {e!s}" - - # Initialize Jira client with OAuth credentials - await task_logger.log_task_progress( - log_entry, - f"Initializing Jira client with OAuth for connector {connector_id}", - {"stage": "client_initialization"}, - ) - - jira_client = JiraConnector( - base_url=credentials.base_url, - access_token=credentials.access_token, - cloud_id=credentials.cloud_id, - ) - else: - # Legacy API token authentication - jira_email = config_data.get("JIRA_EMAIL") - jira_api_token = config_data.get("JIRA_API_TOKEN") - jira_base_url = config_data.get("JIRA_BASE_URL") - - if not jira_email or not jira_api_token or not jira_base_url: - await task_logger.log_task_failure( - log_entry, - f"Jira credentials not found in connector config for connector {connector_id}", - "Missing Jira credentials", - {"error_type": "MissingCredentials"}, - ) - return 0, "Jira credentials not found in connector config" - - # Initialize Jira client with legacy credentials - await task_logger.log_task_progress( - log_entry, - f"Initializing Jira client with API token for connector {connector_id}", - {"stage": "client_initialization"}, - ) - - jira_client = JiraConnector( - base_url=jira_base_url, email=jira_email, api_token=jira_api_token - ) + # Create connector with session and connector_id for internal refresh + # Token refresh will happen automatically when needed + jira_client = JiraHistoryConnector( + session=session, connector_id=connector_id + ) # Calculate date range # Handle "undefined" strings from frontend @@ -231,7 +122,7 @@ async def index_jira_issues( # Get issues within date range try: - issues, error = jira_client.get_issues_by_date_range( + issues, error = await jira_client.get_issues_by_date_range( start_date=start_date_str, end_date=end_date_str, include_comments=True ) @@ -504,6 +395,10 @@ async def index_jira_issues( logger.info( f"JIRA indexing completed: {documents_indexed} new issues, {documents_skipped} skipped" ) + + # Clean up the connector + await jira_client.close() + return ( total_processed, None, @@ -518,6 +413,12 @@ async def index_jira_issues( {"error_type": "SQLAlchemyError"}, ) logger.error(f"Database error: {db_error!s}", exc_info=True) + # Clean up the connector in case of error + if "jira_client" in locals(): + try: + await jira_client.close() + except Exception: + pass return 0, f"Database error: {db_error!s}" except Exception as e: await session.rollback() @@ -528,4 +429,10 @@ async def index_jira_issues( {"error_type": type(e).__name__}, ) logger.error(f"Failed to index JIRA issues: {e!s}", exc_info=True) + # Clean up the connector in case of error + if "jira_client" in locals(): + try: + await jira_client.close() + except Exception: + pass return 0, f"Failed to index JIRA issues: {e!s}" \ No newline at end of file From c7fa640594e874a61276aa5bf9e56a34d4a6c450 Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Tue, 6 Jan 2026 15:04:58 +0530 Subject: [PATCH 25/75] feat: enhance ConfluenceHistoryConnector to support legacy API token authentication - Added support for legacy API token authentication alongside OAuth 2.0 in ConfluenceHistoryConnector. - Implemented logic to handle both authentication methods, ensuring backward compatibility. - Refactored token management to accommodate legacy credentials and updated API request handling accordingly. - Enhanced error handling for credential validation and improved logging for better traceability. --- .../app/connectors/confluence_history.py | 149 +++++++++++++----- 1 file changed, 113 insertions(+), 36 deletions(-) diff --git a/surfsense_backend/app/connectors/confluence_history.py b/surfsense_backend/app/connectors/confluence_history.py index be59e7c12..8247bae70 100644 --- a/surfsense_backend/app/connectors/confluence_history.py +++ b/surfsense_backend/app/connectors/confluence_history.py @@ -12,6 +12,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.future import select from app.config import config +from app.connectors.confluence_connector import ConfluenceConnector from app.db import SearchSourceConnector from app.routes.confluence_add_connector_route import refresh_confluence_token from app.schemas.atlassian_auth_credentials import AtlassianAuthCredentialsBase @@ -26,6 +27,7 @@ class ConfluenceHistoryConnector: This connector uses OAuth 2.0 access tokens to authenticate with the Confluence API. It automatically refreshes expired tokens when needed. + Also supports legacy API token authentication for backward compatibility. """ def __init__( @@ -48,6 +50,10 @@ class ConfluenceHistoryConnector: self._cloud_id: str | None = None self._base_url: str | None = None self._http_client: httpx.AsyncClient | None = None + self._use_oauth = True + self._legacy_email: str | None = None + self._legacy_api_token: str | None = None + self._legacy_confluence_client: ConfluenceConnector | None = None async def _get_valid_token(self) -> str: """ @@ -74,43 +80,58 @@ class ConfluenceHistoryConnector: config_data = connector.config.copy() - # Decrypt credentials if they are encrypted - token_encrypted = config_data.get("_token_encrypted", False) - if token_encrypted and config.SECRET_KEY: + # Check if using OAuth or legacy API token + is_oauth = config_data.get("_token_encrypted", False) or config_data.get("access_token") + + if is_oauth: + # OAuth 2.0 authentication + # Decrypt credentials if they are encrypted + token_encrypted = config_data.get("_token_encrypted", False) + if token_encrypted and config.SECRET_KEY: + try: + token_encryption = TokenEncryption(config.SECRET_KEY) + + # Decrypt sensitive fields + if config_data.get("access_token"): + config_data["access_token"] = token_encryption.decrypt_token( + config_data["access_token"] + ) + if config_data.get("refresh_token"): + config_data["refresh_token"] = token_encryption.decrypt_token( + config_data["refresh_token"] + ) + + logger.info( + f"Decrypted Confluence credentials for connector {self._connector_id}" + ) + except Exception as e: + logger.error( + f"Failed to decrypt Confluence credentials for connector {self._connector_id}: {e!s}" + ) + raise ValueError( + f"Failed to decrypt Confluence credentials: {e!s}" + ) from e + try: - token_encryption = TokenEncryption(config.SECRET_KEY) - - # Decrypt sensitive fields - if config_data.get("access_token"): - config_data["access_token"] = token_encryption.decrypt_token( - config_data["access_token"] - ) - if config_data.get("refresh_token"): - config_data["refresh_token"] = token_encryption.decrypt_token( - config_data["refresh_token"] - ) - - logger.info( - f"Decrypted Confluence credentials for connector {self._connector_id}" - ) + self._credentials = AtlassianAuthCredentialsBase.from_dict(config_data) + # Store cloud_id and base_url for API calls (with backward compatibility for site_url) + self._cloud_id = config_data.get("cloud_id") + self._base_url = config_data.get("base_url") or config_data.get("site_url") + self._use_oauth = True except Exception as e: - logger.error( - f"Failed to decrypt Confluence credentials for connector {self._connector_id}: {e!s}" - ) - raise ValueError( - f"Failed to decrypt Confluence credentials: {e!s}" - ) from e + raise ValueError(f"Invalid Confluence OAuth credentials: {e!s}") from e + else: + # Legacy API token authentication + self._legacy_email = config_data.get("CONFLUENCE_EMAIL") + self._legacy_api_token = config_data.get("CONFLUENCE_API_TOKEN") + self._base_url = config_data.get("CONFLUENCE_BASE_URL") + self._use_oauth = False - try: - self._credentials = AtlassianAuthCredentialsBase.from_dict(config_data) - # Store cloud_id and base_url for API calls (with backward compatibility for site_url) - self._cloud_id = config_data.get("cloud_id") - self._base_url = config_data.get("base_url") or config_data.get("site_url") - except Exception as e: - raise ValueError(f"Invalid Confluence credentials: {e!s}") from e + if not self._legacy_email or not self._legacy_api_token or not self._base_url: + raise ValueError("Confluence credentials not found in connector config") - # Check if token is expired and refreshable - if self._credentials.is_expired and self._credentials.is_refreshable: + # Check if token is expired and refreshable (only for OAuth) + if self._use_oauth and self._credentials.is_expired and self._credentials.is_refreshable: try: logger.info( f"Confluence token expired for connector {self._connector_id}, refreshing..." @@ -167,7 +188,11 @@ class ConfluenceHistoryConnector: f"Failed to refresh Confluence OAuth credentials: {e!s}" ) from e - return self._credentials.access_token + if self._use_oauth: + return self._credentials.access_token + else: + # For legacy auth, return empty string (not used for token-based auth) + return "" async def _get_client(self) -> httpx.AsyncClient: """ @@ -180,6 +205,21 @@ class ConfluenceHistoryConnector: self._http_client = httpx.AsyncClient(timeout=30.0) return self._http_client + async def _get_legacy_client(self) -> ConfluenceConnector: + """ + Get or create ConfluenceConnector with legacy credentials. + + Returns: + ConfluenceConnector instance + """ + if self._legacy_confluence_client is None: + self._legacy_confluence_client = ConfluenceConnector( + base_url=self._base_url, + email=self._legacy_email, + api_token=self._legacy_api_token, + ) + return self._legacy_confluence_client + async def _get_base_url(self) -> str: """ Get the base URL for Confluence API calls. @@ -187,6 +227,10 @@ class ConfluenceHistoryConnector: Returns: Base URL string """ + if not self._use_oauth: + # For legacy auth, use the base_url directly + return self._base_url or "" + if not self._cloud_id: raise ValueError("Cloud ID not available. Cannot construct API URL.") @@ -210,9 +254,22 @@ class ConfluenceHistoryConnector: ValueError: If credentials have not been set Exception: If the API request fails """ + if not self._use_oauth: + # Use legacy ConfluenceConnector for API requests + client = await self._get_legacy_client() + # ConfluenceConnector uses synchronous requests, so we need to handle this differently + # For now, we'll use the legacy client's make_api_request method + # But since it's sync, we'll need to wrap it + import asyncio + loop = asyncio.get_event_loop() + return await loop.run_in_executor( + None, client.make_api_request, endpoint, params + ) + + # OAuth flow token = await self._get_valid_token() base_url = await self._get_base_url() - client = await self._get_client() + http_client = await self._get_client() url = f"{base_url}/wiki/api/v2/{endpoint}" headers = { @@ -222,7 +279,7 @@ class ConfluenceHistoryConnector: } try: - response = await client.get(url, headers=headers, params=params) + response = await http_client.get(url, headers=headers, params=params) response.raise_for_status() return response.json() except httpx.HTTPStatusError as e: @@ -431,6 +488,24 @@ class ConfluenceHistoryConnector: Tuple containing (pages list with comments, error message or None) """ try: + if not self._use_oauth: + # Use legacy ConfluenceConnector for API requests + client = await self._get_legacy_client() + # Ensure credentials are loaded + await self._get_valid_token() + # ConfluenceConnector.get_pages_by_date_range is synchronous + import asyncio + loop = asyncio.get_event_loop() + return await loop.run_in_executor( + None, + client.get_pages_by_date_range, + start_date, + end_date, + space_ids, + include_comments, + ) + + # OAuth flow all_pages = [] if space_ids: @@ -477,6 +552,8 @@ class ConfluenceHistoryConnector: if self._http_client: await self._http_client.aclose() self._http_client = None + # Legacy client doesn't need explicit closing + self._legacy_confluence_client = None async def __aenter__(self): """Async context manager entry.""" From 6ea6e752f6ba7d714c1140ecc24a6bed17bdc545 Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Tue, 6 Jan 2026 15:44:43 +0530 Subject: [PATCH 26/75] refactor: update OAuth scopes and improve connector UI messages - Removed unnecessary OAuth scopes for Confluence and Jira connectors to streamline authentication requirements. - Updated the Confluence and Jira configuration components to enhance user experience by clarifying connection update instructions and improving styling for displayed URLs. --- .../app/routes/confluence_add_connector_route.py | 2 -- surfsense_backend/app/routes/jira_add_connector_route.py | 1 - .../connector-configs/components/confluence-config.tsx | 8 ++++---- .../connector-configs/components/jira-config.tsx | 6 +++--- 4 files changed, 7 insertions(+), 10 deletions(-) diff --git a/surfsense_backend/app/routes/confluence_add_connector_route.py b/surfsense_backend/app/routes/confluence_add_connector_route.py index ee6556543..f651219e1 100644 --- a/surfsense_backend/app/routes/confluence_add_connector_route.py +++ b/surfsense_backend/app/routes/confluence_add_connector_route.py @@ -38,8 +38,6 @@ RESOURCES_URL = "https://api.atlassian.com/oauth/token/accessible-resources" # OAuth scopes for Confluence SCOPES = [ - "read:confluence-content.all", - "read:confluence-space.summary", "read:confluence-user", "read:space:confluence", "read:page:confluence", diff --git a/surfsense_backend/app/routes/jira_add_connector_route.py b/surfsense_backend/app/routes/jira_add_connector_route.py index c7ad835ba..a22260d68 100644 --- a/surfsense_backend/app/routes/jira_add_connector_route.py +++ b/surfsense_backend/app/routes/jira_add_connector_route.py @@ -40,7 +40,6 @@ ACCESSIBLE_RESOURCES_URL = "https://api.atlassian.com/oauth/token/accessible-res # OAuth scopes for Jira SCOPES = [ "read:jira-work", - "write:jira-work", "read:jira-user", "offline_access", # Required for refresh tokens ] diff --git a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/confluence-config.tsx b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/confluence-config.tsx index f757e603a..879a71128 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/confluence-config.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/confluence-config.tsx @@ -80,7 +80,7 @@ export const ConfluenceConfig: FC = ({ // For OAuth connectors, show simple info message if (isOAuth) { - const siteUrl = (connector.config?.site_url as string) || "Unknown"; + const siteUrl = (connector.config?.base_url as string) || (connector.config?.site_url as string) || "Unknown"; return (
{/* OAuth Info */} @@ -94,10 +94,10 @@ export const ConfluenceConfig: FC = ({ This connector is authenticated using OAuth 2.0. Your Confluence instance is:

- {siteUrl} + {siteUrl}

-

- To update your connection, disconnect and reconnect through the OAuth flow. +

+ To update your connection, reconnect this connector.

diff --git a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/jira-config.tsx b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/jira-config.tsx index 158dfdf13..dcc83c2d6 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/jira-config.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/jira-config.tsx @@ -88,10 +88,10 @@ export const JiraConfig: FC = ({ connector, onConfigChange, onN This connector is authenticated using OAuth 2.0. Your Jira instance is:

- {baseUrl} + {baseUrl}

-

- To update your connection, disconnect and reconnect through the OAuth flow. +

+ To update your connection, reconnect this connector.

From c7c5caf559d4296826bc93276bdd5a6a5dec5125 Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Tue, 6 Jan 2026 15:49:31 +0530 Subject: [PATCH 27/75] chore: ran both frontend and backend linting --- .../app/connectors/confluence_history.py | 67 +++++++++++++------ .../app/connectors/jira_connector.py | 8 +-- .../app/connectors/jira_history.py | 33 ++++++--- surfsense_backend/app/routes/__init__.py | 2 +- .../routes/confluence_add_connector_route.py | 32 ++++++--- .../app/routes/discord_add_connector_route.py | 29 +++++--- .../app/routes/jira_add_connector_route.py | 18 +++-- .../app/schemas/atlassian_auth_credentials.py | 3 +- .../app/schemas/discord_auth_credentials.py | 1 - .../connector_indexers/confluence_indexer.py | 12 ++-- .../connector_indexers/discord_indexer.py | 43 ++++++++---- .../tasks/connector_indexers/jira_indexer.py | 10 ++- .../components/confluence-config.tsx | 3 +- .../components/discord-config.tsx | 6 +- .../hooks/use-connector-edit-page.ts | 4 +- 15 files changed, 177 insertions(+), 94 deletions(-) diff --git a/surfsense_backend/app/connectors/confluence_history.py b/surfsense_backend/app/connectors/confluence_history.py index 8247bae70..9e10ffcf1 100644 --- a/surfsense_backend/app/connectors/confluence_history.py +++ b/surfsense_backend/app/connectors/confluence_history.py @@ -24,7 +24,7 @@ logger = logging.getLogger(__name__) class ConfluenceHistoryConnector: """ Confluence connector with OAuth support and automatic token refresh. - + This connector uses OAuth 2.0 access tokens to authenticate with the Confluence API. It automatically refreshes expired tokens when needed. Also supports legacy API token authentication for backward compatibility. @@ -81,8 +81,10 @@ class ConfluenceHistoryConnector: config_data = connector.config.copy() # Check if using OAuth or legacy API token - is_oauth = config_data.get("_token_encrypted", False) or config_data.get("access_token") - + is_oauth = config_data.get("_token_encrypted", False) or config_data.get( + "access_token" + ) + if is_oauth: # OAuth 2.0 authentication # Decrypt credentials if they are encrypted @@ -93,12 +95,16 @@ class ConfluenceHistoryConnector: # Decrypt sensitive fields if config_data.get("access_token"): - config_data["access_token"] = token_encryption.decrypt_token( - config_data["access_token"] + config_data["access_token"] = ( + token_encryption.decrypt_token( + config_data["access_token"] + ) ) if config_data.get("refresh_token"): - config_data["refresh_token"] = token_encryption.decrypt_token( - config_data["refresh_token"] + config_data["refresh_token"] = ( + token_encryption.decrypt_token( + config_data["refresh_token"] + ) ) logger.info( @@ -113,13 +119,19 @@ class ConfluenceHistoryConnector: ) from e try: - self._credentials = AtlassianAuthCredentialsBase.from_dict(config_data) + self._credentials = AtlassianAuthCredentialsBase.from_dict( + config_data + ) # Store cloud_id and base_url for API calls (with backward compatibility for site_url) self._cloud_id = config_data.get("cloud_id") - self._base_url = config_data.get("base_url") or config_data.get("site_url") + self._base_url = config_data.get("base_url") or config_data.get( + "site_url" + ) self._use_oauth = True except Exception as e: - raise ValueError(f"Invalid Confluence OAuth credentials: {e!s}") from e + raise ValueError( + f"Invalid Confluence OAuth credentials: {e!s}" + ) from e else: # Legacy API token authentication self._legacy_email = config_data.get("CONFLUENCE_EMAIL") @@ -127,11 +139,21 @@ class ConfluenceHistoryConnector: self._base_url = config_data.get("CONFLUENCE_BASE_URL") self._use_oauth = False - if not self._legacy_email or not self._legacy_api_token or not self._base_url: - raise ValueError("Confluence credentials not found in connector config") + if ( + not self._legacy_email + or not self._legacy_api_token + or not self._base_url + ): + raise ValueError( + "Confluence credentials not found in connector config" + ) # Check if token is expired and refreshable (only for OAuth) - if self._use_oauth and self._credentials.is_expired and self._credentials.is_refreshable: + if ( + self._use_oauth + and self._credentials.is_expired + and self._credentials.is_refreshable + ): try: logger.info( f"Confluence token expired for connector {self._connector_id}, refreshing..." @@ -170,7 +192,9 @@ class ConfluenceHistoryConnector: self._credentials = AtlassianAuthCredentialsBase.from_dict(config_data) self._cloud_id = config_data.get("cloud_id") # Handle backward compatibility: check both base_url and site_url - self._base_url = config_data.get("base_url") or config_data.get("site_url") + self._base_url = config_data.get("base_url") or config_data.get( + "site_url" + ) # Invalidate cached client so it's recreated with new token if self._http_client: @@ -230,10 +254,10 @@ class ConfluenceHistoryConnector: if not self._use_oauth: # For legacy auth, use the base_url directly return self._base_url or "" - + if not self._cloud_id: raise ValueError("Cloud ID not available. Cannot construct API URL.") - + # Use the Atlassian API format: https://api.atlassian.com/ex/confluence/{cloudid} return f"https://api.atlassian.com/ex/confluence/{self._cloud_id}" @@ -261,11 +285,12 @@ class ConfluenceHistoryConnector: # For now, we'll use the legacy client's make_api_request method # But since it's sync, we'll need to wrap it import asyncio + loop = asyncio.get_event_loop() return await loop.run_in_executor( None, client.make_api_request, endpoint, params ) - + # OAuth flow token = await self._get_valid_token() base_url = await self._get_base_url() @@ -446,7 +471,9 @@ class ConfluenceHistoryConnector: if cursor: params["cursor"] = cursor - result = await self._make_api_request(f"pages/{page_id}/{comment_type}", params) + result = await self._make_api_request( + f"pages/{page_id}/{comment_type}", params + ) if not isinstance(result, dict) or "results" not in result: break # No comments or invalid response @@ -495,6 +522,7 @@ class ConfluenceHistoryConnector: await self._get_valid_token() # ConfluenceConnector.get_pages_by_date_range is synchronous import asyncio + loop = asyncio.get_event_loop() return await loop.run_in_executor( None, @@ -504,7 +532,7 @@ class ConfluenceHistoryConnector: space_ids, include_comments, ) - + # OAuth flow all_pages = [] @@ -562,4 +590,3 @@ class ConfluenceHistoryConnector: async def __aexit__(self, exc_type, exc_val, exc_tb): """Async context manager exit.""" await self.close() - diff --git a/surfsense_backend/app/connectors/jira_connector.py b/surfsense_backend/app/connectors/jira_connector.py index 7bc8f2f03..370460e04 100644 --- a/surfsense_backend/app/connectors/jira_connector.py +++ b/surfsense_backend/app/connectors/jira_connector.py @@ -283,11 +283,11 @@ class JiraConnector: # Query issues that were either created OR updated within the date range # Use end_date + 1 day with < operator to include the full end date from datetime import datetime, timedelta - + # Parse end_date and add 1 day for inclusive end date end_date_obj = datetime.strptime(end_date, "%Y-%m-%d") end_date_next = (end_date_obj + timedelta(days=1)).strftime("%Y-%m-%d") - + # Check both created and updated dates to catch all relevant issues # Use 'created' and 'updated' (standard JQL field names) date_filter = ( @@ -297,9 +297,7 @@ class JiraConnector: jql = f"{date_filter} ORDER BY created DESC" if project_key: - jql = ( - f'project = "{project_key}" AND ({date_filter}) ORDER BY created DESC' - ) + jql = f'project = "{project_key}" AND ({date_filter}) ORDER BY created DESC' # Define fields to retrieve fields = [ diff --git a/surfsense_backend/app/connectors/jira_history.py b/surfsense_backend/app/connectors/jira_history.py index 3e8c69104..6e04ec2a4 100644 --- a/surfsense_backend/app/connectors/jira_history.py +++ b/surfsense_backend/app/connectors/jira_history.py @@ -24,7 +24,7 @@ logger = logging.getLogger(__name__) class JiraHistoryConnector: """ Jira connector with OAuth support and automatic token refresh. - + This connector uses OAuth 2.0 access tokens to authenticate with the Jira API. It automatically refreshes expired tokens when needed. Also supports legacy API token authentication for backward compatibility. @@ -80,8 +80,10 @@ class JiraHistoryConnector: config_data = connector.config.copy() # Check if using OAuth or legacy API token - is_oauth = config_data.get("_token_encrypted", False) or config_data.get("access_token") - + is_oauth = config_data.get("_token_encrypted", False) or config_data.get( + "access_token" + ) + if is_oauth: # OAuth 2.0 authentication if not config.SECRET_KEY: @@ -118,7 +120,9 @@ class JiraHistoryConnector: ) from e try: - self._credentials = AtlassianAuthCredentialsBase.from_dict(config_data) + self._credentials = AtlassianAuthCredentialsBase.from_dict( + config_data + ) self._cloud_id = config_data.get("cloud_id") self._base_url = config_data.get("base_url") self._use_oauth = True @@ -131,11 +135,19 @@ class JiraHistoryConnector: self._base_url = config_data.get("JIRA_BASE_URL") self._use_oauth = False - if not self._legacy_email or not self._legacy_api_token or not self._base_url: + if ( + not self._legacy_email + or not self._legacy_api_token + or not self._base_url + ): raise ValueError("Jira credentials not found in connector config") # Check if token is expired and refreshable (only for OAuth) - if self._use_oauth and self._credentials.is_expired and self._credentials.is_refreshable: + if ( + self._use_oauth + and self._credentials.is_expired + and self._credentials.is_refreshable + ): try: logger.info( f"Jira token expired for connector {self._connector_id}, refreshing..." @@ -206,7 +218,7 @@ class JiraHistoryConnector: if self._use_oauth: # Ensure we have valid token (will refresh if needed) await self._get_valid_token() - + self._jira_client = JiraConnector( base_url=self._base_url, access_token=self._credentials.access_token, @@ -230,7 +242,7 @@ class JiraHistoryConnector: access_token=self._credentials.access_token, cloud_id=self._cloud_id, ) - + return self._jira_client async def get_issues_by_date_range( @@ -256,10 +268,10 @@ class JiraHistoryConnector: # Ensure token is valid (will refresh if needed) if self._use_oauth: await self._get_valid_token() - + # Get client with valid credentials client = await self._get_jira_client() - + # JiraConnector methods are synchronous, so we call them directly # Token refresh has already been handled above return client.get_issues_by_date_range( @@ -317,4 +329,3 @@ class JiraHistoryConnector: async def __aexit__(self, exc_type, exc_val, exc_tb): """Async context manager exit.""" await self.close() - diff --git a/surfsense_backend/app/routes/__init__.py b/surfsense_backend/app/routes/__init__.py index 5b4e24d8f..5015b80c2 100644 --- a/surfsense_backend/app/routes/__init__.py +++ b/surfsense_backend/app/routes/__init__.py @@ -4,6 +4,7 @@ from .airtable_add_connector_route import ( router as airtable_add_connector_router, ) from .circleback_webhook_route import router as circleback_webhook_router +from .confluence_add_connector_route import router as confluence_add_connector_router from .discord_add_connector_route import router as discord_add_connector_router from .documents_routes import router as documents_router from .editor_routes import router as editor_router @@ -17,7 +18,6 @@ from .google_gmail_add_connector_route import ( router as google_gmail_add_connector_router, ) from .jira_add_connector_route import router as jira_add_connector_router -from .confluence_add_connector_route import router as confluence_add_connector_router from .linear_add_connector_route import router as linear_add_connector_router from .logs_routes import router as logs_router from .luma_add_connector_route import router as luma_add_connector_router diff --git a/surfsense_backend/app/routes/confluence_add_connector_route.py b/surfsense_backend/app/routes/confluence_add_connector_route.py index f651219e1..e86d411b6 100644 --- a/surfsense_backend/app/routes/confluence_add_connector_route.py +++ b/surfsense_backend/app/routes/confluence_add_connector_route.py @@ -87,7 +87,9 @@ async def connect_confluence(space_id: int, user: User = Depends(current_active_ raise HTTPException(status_code=400, detail="space_id is required") if not config.ATLASSIAN_CLIENT_ID: - raise HTTPException(status_code=500, detail="Atlassian OAuth not configured.") + raise HTTPException( + status_code=500, detail="Atlassian OAuth not configured." + ) if not config.SECRET_KEY: raise HTTPException( @@ -113,7 +115,9 @@ async def connect_confluence(space_id: int, user: User = Depends(current_active_ auth_url = f"{AUTHORIZATION_URL}?{urlencode(auth_params)}" - logger.info(f"Generated Confluence OAuth URL for user {user.id}, space {space_id}") + logger.info( + f"Generated Confluence OAuth URL for user {user.id}, space {space_id}" + ) return {"auth_url": auth_url} except Exception as e: @@ -216,7 +220,9 @@ async def confluence_callback( error_detail = token_response.text try: error_json = token_response.json() - error_detail = error_json.get("error_description", error_json.get("error", error_detail)) + error_detail = error_json.get( + "error_description", error_json.get("error", error_detail) + ) except Exception: pass raise HTTPException( @@ -252,7 +258,9 @@ async def confluence_callback( break if not cloud_id: - logger.warning("Could not determine Confluence cloud ID from accessible resources") + logger.warning( + "Could not determine Confluence cloud ID from accessible resources" + ) # Calculate expiration time (UTC, tz-aware) expires_at = None @@ -409,7 +417,9 @@ async def refresh_confluence_token( error_detail = token_response.text try: error_json = token_response.json() - error_detail = error_json.get("error_description", error_json.get("error", error_detail)) + error_detail = error_json.get( + "error_description", error_json.get("error", error_detail) + ) except Exception: pass raise HTTPException( @@ -431,7 +441,8 @@ async def refresh_confluence_token( if not access_token: raise HTTPException( - status_code=400, detail="No access token received from Confluence refresh" + status_code=400, + detail="No access token received from Confluence refresh", ) # Update credentials object with encrypted tokens @@ -449,7 +460,9 @@ async def refresh_confluence_token( credentials.cloud_id = connector.config.get("cloud_id") if not credentials.base_url: # Check both base_url and site_url for backward compatibility - credentials.base_url = connector.config.get("base_url") or connector.config.get("site_url") + credentials.base_url = connector.config.get( + "base_url" + ) or connector.config.get("site_url") # Update connector config with encrypted tokens credentials_dict = credentials.to_dict() @@ -458,7 +471,9 @@ async def refresh_confluence_token( await session.commit() await session.refresh(connector) - logger.info(f"Successfully refreshed Confluence token for connector {connector.id}") + logger.info( + f"Successfully refreshed Confluence token for connector {connector.id}" + ) return connector except HTTPException: @@ -468,4 +483,3 @@ async def refresh_confluence_token( raise HTTPException( status_code=500, detail=f"Failed to refresh Confluence token: {e!s}" ) from e - diff --git a/surfsense_backend/app/routes/discord_add_connector_route.py b/surfsense_backend/app/routes/discord_add_connector_route.py index 70a0046a3..6bebac718 100644 --- a/surfsense_backend/app/routes/discord_add_connector_route.py +++ b/surfsense_backend/app/routes/discord_add_connector_route.py @@ -217,7 +217,9 @@ async def discord_callback( error_detail = token_response.text try: error_json = token_response.json() - error_detail = error_json.get("error_description", error_json.get("error", error_detail)) + error_detail = error_json.get( + "error_description", error_json.get("error", error_detail) + ) except Exception: pass raise HTTPException( @@ -263,7 +265,9 @@ async def discord_callback( # Store the bot token from config and OAuth metadata connector_config = { - "bot_token": token_encryption.encrypt_token(bot_token), # Use bot token from config + "bot_token": token_encryption.encrypt_token( + bot_token + ), # Use bot token from config "oauth_access_token": token_encryption.encrypt_token(oauth_access_token) if oauth_access_token else None, # Store OAuth token for reference @@ -356,7 +360,7 @@ async def refresh_discord_token( ) -> SearchSourceConnector: """ Refresh the Discord OAuth tokens for a connector. - + Note: Bot tokens from config don't expire, but OAuth access tokens might. This function refreshes OAuth tokens if needed, but always uses bot token from config. @@ -400,7 +404,9 @@ async def refresh_discord_token( f"No refresh token available for connector {connector.id}. Using bot token from config." ) # Update bot token from config (in case it was changed) - credentials.bot_token = token_encryption.encrypt_token(config.DISCORD_BOT_TOKEN) + credentials.bot_token = token_encryption.encrypt_token( + config.DISCORD_BOT_TOKEN + ) credentials_dict = credentials.to_dict() credentials_dict["_token_encrypted"] = True connector.config = credentials_dict @@ -428,7 +434,9 @@ async def refresh_discord_token( error_detail = token_response.text try: error_json = token_response.json() - error_detail = error_json.get("error_description", error_json.get("error", error_detail)) + error_detail = error_json.get( + "error_description", error_json.get("error", error_detail) + ) except Exception: pass # If refresh fails, bot token from config is still valid @@ -437,7 +445,9 @@ async def refresh_discord_token( "Using bot token from config." ) # Update bot token from config - credentials.bot_token = token_encryption.encrypt_token(config.DISCORD_BOT_TOKEN) + credentials.bot_token = token_encryption.encrypt_token( + config.DISCORD_BOT_TOKEN + ) credentials.refresh_token = None # Clear invalid refresh token credentials_dict = credentials.to_dict() credentials_dict["_token_encrypted"] = True @@ -463,7 +473,7 @@ async def refresh_discord_token( # Always use bot token from config (bot tokens don't expire) credentials.bot_token = token_encryption.encrypt_token(config.DISCORD_BOT_TOKEN) - + # Update OAuth tokens if available if oauth_access_token: # Store OAuth access token for reference @@ -493,7 +503,9 @@ async def refresh_discord_token( await session.commit() await session.refresh(connector) - logger.info(f"Successfully refreshed Discord OAuth tokens for connector {connector.id}") + logger.info( + f"Successfully refreshed Discord OAuth tokens for connector {connector.id}" + ) return connector except HTTPException: @@ -506,4 +518,3 @@ async def refresh_discord_token( raise HTTPException( status_code=500, detail=f"Failed to refresh Discord tokens: {e!s}" ) from e - diff --git a/surfsense_backend/app/routes/jira_add_connector_route.py b/surfsense_backend/app/routes/jira_add_connector_route.py index a22260d68..740c30300 100644 --- a/surfsense_backend/app/routes/jira_add_connector_route.py +++ b/surfsense_backend/app/routes/jira_add_connector_route.py @@ -86,7 +86,9 @@ async def connect_jira(space_id: int, user: User = Depends(current_active_user)) raise HTTPException(status_code=400, detail="space_id is required") if not config.ATLASSIAN_CLIENT_ID: - raise HTTPException(status_code=500, detail="Atlassian OAuth not configured.") + raise HTTPException( + status_code=500, detail="Atlassian OAuth not configured." + ) if not config.SECRET_KEY: raise HTTPException( @@ -215,7 +217,9 @@ async def jira_callback( error_detail = token_response.text try: error_json = token_response.json() - error_detail = error_json.get("error_description", error_json.get("error", error_detail)) + error_detail = error_json.get( + "error_description", error_json.get("error", error_detail) + ) except Exception: pass raise HTTPException( @@ -254,9 +258,7 @@ async def jira_callback( # Filter for Jira instances (resources with type "jira" or id field) jira_instances = [ - r - for r in resources - if r.get("id") and (r.get("name") or r.get("url")) + r for r in resources if r.get("id") and (r.get("name") or r.get("url")) ] if not jira_instances: @@ -270,7 +272,7 @@ async def jira_callback( jira_instance = jira_instances[0] cloud_id = jira_instance["id"] base_url = jira_instance.get("url") - + # If URL is not provided, construct it from cloud_id if not base_url: # Try to extract from name or construct default format @@ -433,7 +435,9 @@ async def refresh_jira_token( error_detail = token_response.text try: error_json = token_response.json() - error_detail = error_json.get("error_description", error_json.get("error", error_detail)) + error_detail = error_json.get( + "error_description", error_json.get("error", error_detail) + ) except Exception: pass raise HTTPException( diff --git a/surfsense_backend/app/schemas/atlassian_auth_credentials.py b/surfsense_backend/app/schemas/atlassian_auth_credentials.py index 3290e5d67..cbb4772e6 100644 --- a/surfsense_backend/app/schemas/atlassian_auth_credentials.py +++ b/surfsense_backend/app/schemas/atlassian_auth_credentials.py @@ -13,7 +13,7 @@ from pydantic import BaseModel, field_validator class AtlassianAuthCredentialsBase(BaseModel): """ Base model for Atlassian OAuth 2.0 credentials. - + Used for both Jira and Confluence connectors since they share the same Atlassian OAuth infrastructure and token structure. """ @@ -84,4 +84,3 @@ class AtlassianAuthCredentialsBase(BaseModel): if isinstance(v, datetime): return v if v.tzinfo else v.replace(tzinfo=UTC) return v - diff --git a/surfsense_backend/app/schemas/discord_auth_credentials.py b/surfsense_backend/app/schemas/discord_auth_credentials.py index 0c18a7554..7ea4ee55c 100644 --- a/surfsense_backend/app/schemas/discord_auth_credentials.py +++ b/surfsense_backend/app/schemas/discord_auth_credentials.py @@ -73,4 +73,3 @@ class DiscordAuthCredentialsBase(BaseModel): if isinstance(v, datetime): return v if v.tzinfo else v.replace(tzinfo=UTC) return v - diff --git a/surfsense_backend/app/tasks/connector_indexers/confluence_indexer.py b/surfsense_backend/app/tasks/connector_indexers/confluence_indexer.py index 85aa10e1a..09022a30b 100644 --- a/surfsense_backend/app/tasks/connector_indexers/confluence_indexer.py +++ b/surfsense_backend/app/tasks/connector_indexers/confluence_indexer.py @@ -90,9 +90,11 @@ async def index_confluence_pages( {"stage": "client_initialization"}, ) - confluence_client: ConfluenceHistoryConnector | None = ConfluenceHistoryConnector( - session=session, - connector_id=connector_id, + confluence_client: ConfluenceHistoryConnector | None = ( + ConfluenceHistoryConnector( + session=session, + connector_id=connector_id, + ) ) # Calculate date range @@ -421,11 +423,11 @@ async def index_confluence_pages( logger.info( f"Confluence indexing completed: {documents_indexed} new pages, {documents_skipped} skipped" ) - + # Close the client connection if confluence_client: await confluence_client.close() - + return ( total_processed, None, diff --git a/surfsense_backend/app/tasks/connector_indexers/discord_indexer.py b/surfsense_backend/app/tasks/connector_indexers/discord_indexer.py index 5c92d2601..110732831 100644 --- a/surfsense_backend/app/tasks/connector_indexers/discord_indexer.py +++ b/surfsense_backend/app/tasks/connector_indexers/discord_indexer.py @@ -69,7 +69,9 @@ async def index_discord_messages( try: # Normalize date parameters - handle 'undefined' strings from frontend - if start_date and (start_date.lower() == "undefined" or start_date.strip() == ""): + if start_date and ( + start_date.lower() == "undefined" or start_date.strip() == "" + ): start_date = None if end_date and (end_date.lower() == "undefined" or end_date.strip() == ""): end_date = None @@ -118,12 +120,13 @@ async def index_discord_messages( elif has_legacy: # Backward compatibility: use legacy token format discord_token = connector.config.get("DISCORD_BOT_TOKEN") - + # Decrypt token if it's encrypted (legacy tokens might be encrypted) token_encrypted = connector.config.get("_token_encrypted", False) if token_encrypted and config.SECRET_KEY and discord_token: try: from app.utils.oauth_security import TokenEncryption + token_encryption = TokenEncryption(config.SECRET_KEY) discord_token = token_encryption.decrypt_token(discord_token) logger.info( @@ -135,7 +138,7 @@ async def index_discord_messages( "Trying to use token as-is (might be unencrypted)." ) # Continue with token as-is - might be unencrypted legacy token - + discord_client = DiscordConnector(token=discord_token) else: await task_logger.log_task_failure( @@ -210,11 +213,16 @@ async def index_discord_messages( f"Date parsing error: {e!s}", {"error_type": "InvalidDateFormat", "start_date": start_date}, ) - return 0, f"Invalid start_date format: {start_date}. Expected YYYY-MM-DD format." + return ( + 0, + f"Invalid start_date format: {start_date}. Expected YYYY-MM-DD format.", + ) try: end_date_iso = ( - datetime.strptime(end_date, "%Y-%m-%d").replace(tzinfo=UTC).isoformat() + datetime.strptime(end_date, "%Y-%m-%d") + .replace(tzinfo=UTC) + .isoformat() ) except ValueError as e: await task_logger.log_task_failure( @@ -223,7 +231,10 @@ async def index_discord_messages( f"Date parsing error: {e!s}", {"error_type": "InvalidDateFormat", "end_date": end_date}, ) - return 0, f"Invalid end_date format: {end_date}. Expected YYYY-MM-DD format." + return ( + 0, + f"Invalid end_date format: {end_date}. Expected YYYY-MM-DD format.", + ) logger.info( f"Indexing Discord messages from {start_date_iso} to {end_date_iso}" @@ -384,8 +395,10 @@ async def index_discord_messages( ) # Check if document with this unique identifier already exists - existing_document = await check_document_by_unique_identifier( - session, unique_identifier_hash + existing_document = ( + await check_document_by_unique_identifier( + session, unique_identifier_hash + ) ) if existing_document: @@ -406,8 +419,10 @@ async def index_discord_messages( chunks = await create_document_chunks( combined_document_string ) - doc_embedding = config.embedding_model_instance.embed( - combined_document_string + doc_embedding = ( + config.embedding_model_instance.embed( + combined_document_string + ) ) # Update existing document @@ -429,7 +444,9 @@ async def index_discord_messages( # Delete old chunks and add new ones existing_document.chunks = chunks - existing_document.updated_at = get_current_timestamp() + existing_document.updated_at = ( + get_current_timestamp() + ) documents_indexed += 1 logger.info( @@ -439,7 +456,9 @@ async def index_discord_messages( # Document doesn't exist - create new one # Process chunks - chunks = await create_document_chunks(combined_document_string) + chunks = await create_document_chunks( + combined_document_string + ) doc_embedding = config.embedding_model_instance.embed( combined_document_string ) diff --git a/surfsense_backend/app/tasks/connector_indexers/jira_indexer.py b/surfsense_backend/app/tasks/connector_indexers/jira_indexer.py index 47ad0986f..7209deb49 100644 --- a/surfsense_backend/app/tasks/connector_indexers/jira_indexer.py +++ b/surfsense_backend/app/tasks/connector_indexers/jira_indexer.py @@ -95,9 +95,7 @@ async def index_jira_issues( # Create connector with session and connector_id for internal refresh # Token refresh will happen automatically when needed - jira_client = JiraHistoryConnector( - session=session, connector_id=connector_id - ) + jira_client = JiraHistoryConnector(session=session, connector_id=connector_id) # Calculate date range # Handle "undefined" strings from frontend @@ -395,10 +393,10 @@ async def index_jira_issues( logger.info( f"JIRA indexing completed: {documents_indexed} new issues, {documents_skipped} skipped" ) - + # Clean up the connector await jira_client.close() - + return ( total_processed, None, @@ -435,4 +433,4 @@ async def index_jira_issues( await jira_client.close() except Exception: pass - return 0, f"Failed to index JIRA issues: {e!s}" \ No newline at end of file + return 0, f"Failed to index JIRA issues: {e!s}" diff --git a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/confluence-config.tsx b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/confluence-config.tsx index 879a71128..59fa89554 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/confluence-config.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/confluence-config.tsx @@ -80,7 +80,8 @@ export const ConfluenceConfig: FC = ({ // For OAuth connectors, show simple info message if (isOAuth) { - const siteUrl = (connector.config?.base_url as string) || (connector.config?.site_url as string) || "Unknown"; + const siteUrl = + (connector.config?.base_url as string) || (connector.config?.site_url as string) || "Unknown"; return (
{/* OAuth Info */} diff --git a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/discord-config.tsx b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/discord-config.tsx index 464bc438f..dd4c89c8e 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/discord-config.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/discord-config.tsx @@ -18,9 +18,9 @@ export const DiscordConfig: FC = () => {

Add Bot to Servers

- Before indexing, make sure the Discord bot has been added to the servers (guilds) you want to - index. The bot can only access messages from servers it's been added to. Use the OAuth - authorization flow to add the bot to your servers. + Before indexing, make sure the Discord bot has been added to the servers (guilds) you + want to index. The bot can only access messages from servers it's been added to. Use the + OAuth authorization flow to add the bot to your servers.

diff --git a/surfsense_web/hooks/use-connector-edit-page.ts b/surfsense_web/hooks/use-connector-edit-page.ts index 5eb55bf1c..a1a3c88f4 100644 --- a/surfsense_web/hooks/use-connector-edit-page.ts +++ b/surfsense_web/hooks/use-connector-edit-page.ts @@ -449,13 +449,13 @@ export function useConnectorEditPage(connectorId: number, searchSpaceId: string) case "JIRA_CONNECTOR": { // Check if this is an OAuth connector (has access_token or _token_encrypted flag) const isJiraOAuth = !!(originalConfig.access_token || originalConfig._token_encrypted); - + if (isJiraOAuth) { // OAuth connectors don't allow editing credentials through the form // Only allow name changes, which are handled separately break; } - + // Legacy API token connector - allow editing credentials if ( formData.JIRA_BASE_URL !== originalConfig.JIRA_BASE_URL || From 929bc026e64dde20c95306c28a643f14161aa2f0 Mon Sep 17 00:00:00 2001 From: "DESKTOP-RTLN3BA\\$punk" Date: Tue, 6 Jan 2026 03:09:49 -0800 Subject: [PATCH 28/75] refactor: remove react-markdown dependency and enhance document editing logic - Removed the react-markdown dependency from package.json and pnpm-lock.yaml. - Introduced a constant for editable document types in RowActions component to streamline edit functionality. - Updated RowActions to conditionally render edit options based on document type, improving user experience. --- .../(manage)/components/RowActions.tsx | 61 +++++++++++-------- surfsense_web/components/markdown-viewer.tsx | 5 +- surfsense_web/package.json | 1 - surfsense_web/pnpm-lock.yaml | 3 - 4 files changed, 38 insertions(+), 32 deletions(-) diff --git a/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/RowActions.tsx b/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/RowActions.tsx index d6da0fb6a..2fe9ab3e8 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/RowActions.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/RowActions.tsx @@ -25,6 +25,9 @@ import { import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import type { Document } from "./types"; +// Only FILE and NOTE document types can be edited +const EDITABLE_DOCUMENT_TYPES = ["FILE", "NOTE"] as const; + export function RowActions({ document, deleteDocument, @@ -41,6 +44,10 @@ export function RowActions({ const [isDeleting, setIsDeleting] = useState(false); const router = useRouter(); + const isEditable = EDITABLE_DOCUMENT_TYPES.includes( + document.document_type as (typeof EDITABLE_DOCUMENT_TYPES)[number] + ); + const handleDelete = async () => { setIsDeleting(true); try { @@ -65,28 +72,30 @@ export function RowActions({
{/* Desktop Actions */}
- - - - - - - -

Edit Document

-
-
+ + + + +

Edit Document

+
+ + )} @@ -146,10 +155,12 @@ export function RowActions({ - - - Edit - + {isEditable && ( + + + Edit + + )} setIsMetadataOpen(true)}> Metadata diff --git a/surfsense_web/components/markdown-viewer.tsx b/surfsense_web/components/markdown-viewer.tsx index 5318ba5d1..407adba7a 100644 --- a/surfsense_web/components/markdown-viewer.tsx +++ b/surfsense_web/components/markdown-viewer.tsx @@ -1,6 +1,5 @@ import Image from "next/image"; -import type { Components } from "react-markdown"; -import { Streamdown } from "streamdown"; +import { type StreamdownProps, Streamdown } from "streamdown"; import { cn } from "@/lib/utils"; interface MarkdownViewerProps { @@ -9,7 +8,7 @@ interface MarkdownViewerProps { } export function MarkdownViewer({ content, className }: MarkdownViewerProps) { - const components: Components = { + const components: StreamdownProps["components"] = { // Define custom components for markdown elements p: ({ children, ...props }) => (

diff --git a/surfsense_web/package.json b/surfsense_web/package.json index 2a6eaf8a6..ccb34b973 100644 --- a/surfsense_web/package.json +++ b/surfsense_web/package.json @@ -89,7 +89,6 @@ "react-dropzone": "^14.3.8", "react-hook-form": "^7.61.1", "react-json-view-lite": "^2.4.1", - "react-markdown": "^10.1.0", "react-syntax-highlighter": "^15.6.1", "react-wrap-balancer": "^1.1.1", "rehype-raw": "^7.0.0", diff --git a/surfsense_web/pnpm-lock.yaml b/surfsense_web/pnpm-lock.yaml index f11cb77b9..a184b6cd0 100644 --- a/surfsense_web/pnpm-lock.yaml +++ b/surfsense_web/pnpm-lock.yaml @@ -212,9 +212,6 @@ importers: react-json-view-lite: specifier: ^2.4.1 version: 2.5.0(react@19.2.3) - react-markdown: - specifier: ^10.1.0 - version: 10.1.0(@types/react@19.2.7)(react@19.2.3) react-syntax-highlighter: specifier: ^15.6.1 version: 15.6.6(react@19.2.3) From ba54e1da0615d71ff8649491149e378f773f4c2b Mon Sep 17 00:00:00 2001 From: "DESKTOP-RTLN3BA\\$punk" Date: Tue, 6 Jan 2026 03:39:25 -0800 Subject: [PATCH 29/75] feat: update documentation structure and remove 'full' property from MDX files - Enhanced the DocsPage component by adding table of content and popover configurations. - Removed the 'full' property from multiple MDX files to streamline documentation structure. - Updated meta.json to reflect new documentation organization and added a 'connectors' page. --- surfsense_web/app/docs/[[...slug]]/page.tsx | 12 +++++- .../content/docs/connectors/airtable.mdx | 33 ++++++++++++++++ .../content/docs/connectors/bookstack.mdx | 33 ++++++++++++++++ .../content/docs/connectors/clickup.mdx | 33 ++++++++++++++++ .../content/docs/connectors/confluence.mdx | 34 +++++++++++++++++ .../content/docs/connectors/discord.mdx | 32 ++++++++++++++++ .../content/docs/connectors/elasticsearch.mdx | 32 ++++++++++++++++ .../content/docs/connectors/github.mdx | 34 +++++++++++++++++ .../content/docs/connectors/gmail.mdx | 34 +++++++++++++++++ .../docs/connectors/google-calendar.mdx | 34 +++++++++++++++++ .../content/docs/connectors/google-drive.mdx | 34 +++++++++++++++++ .../content/docs/connectors/jira.mdx | 34 +++++++++++++++++ .../content/docs/connectors/linear.mdx | 33 ++++++++++++++++ .../content/docs/connectors/luma.mdx | 33 ++++++++++++++++ .../content/docs/connectors/meta.json | 23 +++++++++++ .../content/docs/connectors/notion.mdx | 33 ++++++++++++++++ .../content/docs/connectors/slack.mdx | 33 ++++++++++++++++ .../content/docs/connectors/web-crawler.mdx | 38 +++++++++++++++++++ .../content/docs/docker-installation.mdx | 1 - surfsense_web/content/docs/index.mdx | 1 - surfsense_web/content/docs/installation.mdx | 1 - .../content/docs/manual-installation.mdx | 1 - surfsense_web/content/docs/meta.json | 13 +++++-- 23 files changed, 581 insertions(+), 8 deletions(-) create mode 100644 surfsense_web/content/docs/connectors/airtable.mdx create mode 100644 surfsense_web/content/docs/connectors/bookstack.mdx create mode 100644 surfsense_web/content/docs/connectors/clickup.mdx create mode 100644 surfsense_web/content/docs/connectors/confluence.mdx create mode 100644 surfsense_web/content/docs/connectors/discord.mdx create mode 100644 surfsense_web/content/docs/connectors/elasticsearch.mdx create mode 100644 surfsense_web/content/docs/connectors/github.mdx create mode 100644 surfsense_web/content/docs/connectors/gmail.mdx create mode 100644 surfsense_web/content/docs/connectors/google-calendar.mdx create mode 100644 surfsense_web/content/docs/connectors/google-drive.mdx create mode 100644 surfsense_web/content/docs/connectors/jira.mdx create mode 100644 surfsense_web/content/docs/connectors/linear.mdx create mode 100644 surfsense_web/content/docs/connectors/luma.mdx create mode 100644 surfsense_web/content/docs/connectors/meta.json create mode 100644 surfsense_web/content/docs/connectors/notion.mdx create mode 100644 surfsense_web/content/docs/connectors/slack.mdx create mode 100644 surfsense_web/content/docs/connectors/web-crawler.mdx diff --git a/surfsense_web/app/docs/[[...slug]]/page.tsx b/surfsense_web/app/docs/[[...slug]]/page.tsx index 1280dfb25..204148bf9 100644 --- a/surfsense_web/app/docs/[[...slug]]/page.tsx +++ b/surfsense_web/app/docs/[[...slug]]/page.tsx @@ -11,7 +11,17 @@ export default async function Page(props: { params: Promise<{ slug?: string[] }> const MDX = page.data.body; return ( - + {page.data.title} {page.data.description} diff --git a/surfsense_web/content/docs/connectors/airtable.mdx b/surfsense_web/content/docs/connectors/airtable.mdx new file mode 100644 index 000000000..647208271 --- /dev/null +++ b/surfsense_web/content/docs/connectors/airtable.mdx @@ -0,0 +1,33 @@ +--- +title: Airtable +description: Connect your Airtable bases to SurfSense +--- + +# Airtable Connector + +Index your Airtable bases, tables, and records. + +## Prerequisites + +- An Airtable account +- API access to the bases you want to connect + +## Setup + +1. Navigate to your Search Space settings +2. Click on **Add Connector** +3. Select **Airtable** from the list +4. Enter your Airtable API key +5. Select the bases and tables you want to index + +## What Gets Indexed + +- Table records +- Field values +- Attachments +- Linked records + +## Sync Frequency + +The Airtable connector supports scheduled syncing to keep your data up to date. + diff --git a/surfsense_web/content/docs/connectors/bookstack.mdx b/surfsense_web/content/docs/connectors/bookstack.mdx new file mode 100644 index 000000000..c05a6376b --- /dev/null +++ b/surfsense_web/content/docs/connectors/bookstack.mdx @@ -0,0 +1,33 @@ +--- +title: Bookstack +description: Connect your Bookstack instance to SurfSense +--- + +# Bookstack Connector + +Index your Bookstack books, chapters, and pages. + +## Prerequisites + +- A Bookstack instance +- API access credentials + +## Setup + +1. Navigate to your Search Space settings +2. Click on **Add Connector** +3. Select **Bookstack** from the list +4. Enter your Bookstack instance URL and API credentials +5. Select the shelves and books you want to index + +## What Gets Indexed + +- Books and chapters +- Pages and content +- Attachments +- Tags and metadata + +## Sync Frequency + +The Bookstack connector supports scheduled syncing to keep your content up to date. + diff --git a/surfsense_web/content/docs/connectors/clickup.mdx b/surfsense_web/content/docs/connectors/clickup.mdx new file mode 100644 index 000000000..90734555a --- /dev/null +++ b/surfsense_web/content/docs/connectors/clickup.mdx @@ -0,0 +1,33 @@ +--- +title: ClickUp +description: Connect your ClickUp workspace to SurfSense +--- + +# ClickUp Connector + +Sync your ClickUp tasks, docs, and content to SurfSense. + +## Prerequisites + +- A ClickUp account +- Access to the workspaces you want to connect + +## Setup + +1. Navigate to your Search Space settings +2. Click on **Add Connector** +3. Select **ClickUp** from the list +4. Authorize SurfSense to access your ClickUp workspace +5. Select the spaces and folders you want to index + +## What Gets Indexed + +- Tasks and subtasks +- Task descriptions and comments +- ClickUp Docs +- Custom fields + +## Sync Frequency + +The ClickUp connector supports scheduled syncing to keep your content up to date. + diff --git a/surfsense_web/content/docs/connectors/confluence.mdx b/surfsense_web/content/docs/connectors/confluence.mdx new file mode 100644 index 000000000..17643a86d --- /dev/null +++ b/surfsense_web/content/docs/connectors/confluence.mdx @@ -0,0 +1,34 @@ +--- +title: Confluence +description: Connect your Confluence spaces to SurfSense +--- + +# Confluence Connector + +Index your Confluence pages, spaces, and documentation. + +## Prerequisites + +- A Confluence account (Cloud or Data Center) +- Access to the spaces you want to connect + +## Setup + +1. Navigate to your Search Space settings +2. Click on **Add Connector** +3. Select **Confluence** from the list +4. Enter your Confluence instance URL and credentials +5. Select the spaces you want to index + +## What Gets Indexed + +- Pages and blog posts +- Page comments +- Attachments +- Space documentation +- Page hierarchy + +## Sync Frequency + +The Confluence connector supports scheduled syncing to keep your content up to date. + diff --git a/surfsense_web/content/docs/connectors/discord.mdx b/surfsense_web/content/docs/connectors/discord.mdx new file mode 100644 index 000000000..2dd2c1205 --- /dev/null +++ b/surfsense_web/content/docs/connectors/discord.mdx @@ -0,0 +1,32 @@ +--- +title: Discord +description: Connect your Discord servers to SurfSense +--- + +# Discord Connector + +Index your Discord server conversations and content. + +## Prerequisites + +- A Discord account +- Server admin permissions + +## Setup + +1. Navigate to your Search Space settings +2. Click on **Add Connector** +3. Select **Discord** from the list +4. Authorize SurfSense to access your Discord server +5. Select the channels you want to index + +## What Gets Indexed + +- Text channel messages +- Thread messages +- Shared files and links + +## Sync Frequency + +The Discord connector supports scheduled syncing to keep your content up to date. + diff --git a/surfsense_web/content/docs/connectors/elasticsearch.mdx b/surfsense_web/content/docs/connectors/elasticsearch.mdx new file mode 100644 index 000000000..8fb003253 --- /dev/null +++ b/surfsense_web/content/docs/connectors/elasticsearch.mdx @@ -0,0 +1,32 @@ +--- +title: Elasticsearch +description: Connect your Elasticsearch cluster to SurfSense +--- + +# Elasticsearch Connector + +Index data from your Elasticsearch cluster. + +## Prerequisites + +- An Elasticsearch cluster +- Access credentials + +## Setup + +1. Navigate to your Search Space settings +2. Click on **Add Connector** +3. Select **Elasticsearch** from the list +4. Enter your Elasticsearch cluster URL and credentials +5. Configure the indices you want to index + +## What Gets Indexed + +- Documents from specified indices +- Custom field mappings +- Metadata + +## Sync Frequency + +The Elasticsearch connector supports scheduled syncing to keep your data up to date. + diff --git a/surfsense_web/content/docs/connectors/github.mdx b/surfsense_web/content/docs/connectors/github.mdx new file mode 100644 index 000000000..90bb91a96 --- /dev/null +++ b/surfsense_web/content/docs/connectors/github.mdx @@ -0,0 +1,34 @@ +--- +title: GitHub +description: Connect your GitHub repositories to SurfSense +--- + +# GitHub Connector + +Index your GitHub repositories, issues, pull requests, and documentation. + +## Prerequisites + +- A GitHub account +- Access to the repositories you want to connect + +## Setup + +1. Navigate to your Search Space settings +2. Click on **Add Connector** +3. Select **GitHub** from the list +4. Authorize SurfSense to access your GitHub account +5. Select the repositories you want to index + +## What Gets Indexed + +- Repository README and documentation +- Issues and issue comments +- Pull requests and PR comments +- Code files (configurable) +- Discussions + +## Sync Frequency + +The GitHub connector supports scheduled syncing to keep your content up to date. + diff --git a/surfsense_web/content/docs/connectors/gmail.mdx b/surfsense_web/content/docs/connectors/gmail.mdx new file mode 100644 index 000000000..ac5486ce6 --- /dev/null +++ b/surfsense_web/content/docs/connectors/gmail.mdx @@ -0,0 +1,34 @@ +--- +title: Gmail +description: Connect your Gmail to SurfSense +--- + +# Gmail Connector + +Index your Gmail emails and make them searchable. + +## Prerequisites + +- A Google account +- Google OAuth configured in SurfSense (see [Prerequisites](/docs)) +- Gmail API enabled in Google Cloud Console + +## Setup + +1. Navigate to your Search Space settings +2. Click on **Add Connector** +3. Select **Gmail** from the list +4. Authorize SurfSense to access your Gmail +5. Configure which labels/folders to index + +## What Gets Indexed + +- Email content +- Email attachments +- Thread conversations +- Labels and categories + +## Sync Frequency + +The Gmail connector supports scheduled syncing to keep your emails indexed. + diff --git a/surfsense_web/content/docs/connectors/google-calendar.mdx b/surfsense_web/content/docs/connectors/google-calendar.mdx new file mode 100644 index 000000000..76b3ea588 --- /dev/null +++ b/surfsense_web/content/docs/connectors/google-calendar.mdx @@ -0,0 +1,34 @@ +--- +title: Google Calendar +description: Connect your Google Calendar to SurfSense +--- + +# Google Calendar Connector + +Index your Google Calendar events and make them searchable. + +## Prerequisites + +- A Google account +- Google OAuth configured in SurfSense (see [Prerequisites](/docs)) +- Google Calendar API enabled in Google Cloud Console + +## Setup + +1. Navigate to your Search Space settings +2. Click on **Add Connector** +3. Select **Google Calendar** from the list +4. Authorize SurfSense to access your Google Calendar +5. Select which calendars to index + +## What Gets Indexed + +- Event titles and descriptions +- Event attendees +- Meeting notes +- Recurring events + +## Sync Frequency + +The Google Calendar connector supports scheduled syncing to keep your events indexed. + diff --git a/surfsense_web/content/docs/connectors/google-drive.mdx b/surfsense_web/content/docs/connectors/google-drive.mdx new file mode 100644 index 000000000..6538e24b5 --- /dev/null +++ b/surfsense_web/content/docs/connectors/google-drive.mdx @@ -0,0 +1,34 @@ +--- +title: Google Drive +description: Connect your Google Drive to SurfSense +--- + +# Google Drive Connector + +Index your Google Drive files, documents, and shared content. + +## Prerequisites + +- A Google account +- Google OAuth configured in SurfSense (see [Prerequisites](/docs)) + +## Setup + +1. Navigate to your Search Space settings +2. Click on **Add Connector** +3. Select **Google Drive** from the list +4. Authorize SurfSense to access your Google Drive +5. Select the folders you want to index + +## What Gets Indexed + +- Google Docs +- Google Sheets +- Google Slides +- PDFs and other documents +- Shared files + +## Sync Frequency + +The Google Drive connector supports scheduled syncing to keep your content up to date. + diff --git a/surfsense_web/content/docs/connectors/jira.mdx b/surfsense_web/content/docs/connectors/jira.mdx new file mode 100644 index 000000000..9c086d24b --- /dev/null +++ b/surfsense_web/content/docs/connectors/jira.mdx @@ -0,0 +1,34 @@ +--- +title: Jira +description: Connect your Jira projects to SurfSense +--- + +# Jira Connector + +Sync your Jira issues, projects, and documentation to SurfSense. + +## Prerequisites + +- A Jira account (Cloud or Data Center) +- Access to the projects you want to connect + +## Setup + +1. Navigate to your Search Space settings +2. Click on **Add Connector** +3. Select **Jira** from the list +4. Enter your Jira instance URL and credentials +5. Select the projects you want to index + +## What Gets Indexed + +- Issues and subtasks +- Issue descriptions and comments +- Attachments +- Custom fields +- Project documentation + +## Sync Frequency + +The Jira connector supports scheduled syncing to keep your content up to date. + diff --git a/surfsense_web/content/docs/connectors/linear.mdx b/surfsense_web/content/docs/connectors/linear.mdx new file mode 100644 index 000000000..20b31cabc --- /dev/null +++ b/surfsense_web/content/docs/connectors/linear.mdx @@ -0,0 +1,33 @@ +--- +title: Linear +description: Connect your Linear workspace to SurfSense +--- + +# Linear Connector + +Sync your Linear issues, projects, and documentation to SurfSense. + +## Prerequisites + +- A Linear account +- Access to the teams you want to connect + +## Setup + +1. Navigate to your Search Space settings +2. Click on **Add Connector** +3. Select **Linear** from the list +4. Authorize SurfSense to access your Linear workspace +5. Select the teams and projects you want to index + +## What Gets Indexed + +- Issues and sub-issues +- Issue descriptions and comments +- Project documentation +- Roadmap items + +## Sync Frequency + +The Linear connector supports scheduled syncing to keep your content up to date. + diff --git a/surfsense_web/content/docs/connectors/luma.mdx b/surfsense_web/content/docs/connectors/luma.mdx new file mode 100644 index 000000000..8a244df07 --- /dev/null +++ b/surfsense_web/content/docs/connectors/luma.mdx @@ -0,0 +1,33 @@ +--- +title: Luma +description: Connect your Luma events to SurfSense +--- + +# Luma Connector + +Index your Luma events and event content. + +## Prerequisites + +- A Luma account +- API access + +## Setup + +1. Navigate to your Search Space settings +2. Click on **Add Connector** +3. Select **Luma** from the list +4. Authorize SurfSense to access your Luma account +5. Select the events you want to index + +## What Gets Indexed + +- Event details and descriptions +- Event schedules +- Attendee information (if authorized) +- Event updates + +## Sync Frequency + +The Luma connector supports scheduled syncing to keep your events up to date. + diff --git a/surfsense_web/content/docs/connectors/meta.json b/surfsense_web/content/docs/connectors/meta.json new file mode 100644 index 000000000..2515bc7d8 --- /dev/null +++ b/surfsense_web/content/docs/connectors/meta.json @@ -0,0 +1,23 @@ +{ + "title": "Connectors", + "pages": [ + "notion", + "slack", + "discord", + "clickup", + "github", + "jira", + "linear", + "google-drive", + "gmail", + "google-calendar", + "confluence", + "bookstack", + "airtable", + "elasticsearch", + "web-crawler", + "luma" + ], + "defaultOpen": true +} + diff --git a/surfsense_web/content/docs/connectors/notion.mdx b/surfsense_web/content/docs/connectors/notion.mdx new file mode 100644 index 000000000..784875e3c --- /dev/null +++ b/surfsense_web/content/docs/connectors/notion.mdx @@ -0,0 +1,33 @@ +--- +title: Notion +description: Connect your Notion workspaces to SurfSense +--- + +# Notion Connector + +Connect your Notion workspaces to index pages, databases, and content. + +## Prerequisites + +- A Notion account +- Access to the workspaces you want to connect + +## Setup + +1. Navigate to your Search Space settings +2. Click on **Add Connector** +3. Select **Notion** from the list +4. Authorize SurfSense to access your Notion workspace +5. Select the pages and databases you want to index + +## What Gets Indexed + +- Pages and subpages +- Database entries +- Comments and discussions +- Embedded content + +## Sync Frequency + +The Notion connector supports scheduled syncing to keep your content up to date. + diff --git a/surfsense_web/content/docs/connectors/slack.mdx b/surfsense_web/content/docs/connectors/slack.mdx new file mode 100644 index 000000000..089ccf67d --- /dev/null +++ b/surfsense_web/content/docs/connectors/slack.mdx @@ -0,0 +1,33 @@ +--- +title: Slack +description: Connect your Slack workspace to SurfSense +--- + +# Slack Connector + +Index your Slack conversations and make them searchable. + +## Prerequisites + +- A Slack workspace +- Admin permissions to install apps + +## Setup + +1. Navigate to your Search Space settings +2. Click on **Add Connector** +3. Select **Slack** from the list +4. Authorize SurfSense to access your Slack workspace +5. Select the channels you want to index + +## What Gets Indexed + +- Public channel messages +- Private channels (if authorized) +- Thread replies +- Shared files and links + +## Sync Frequency + +The Slack connector supports scheduled syncing to keep your conversations indexed. + diff --git a/surfsense_web/content/docs/connectors/web-crawler.mdx b/surfsense_web/content/docs/connectors/web-crawler.mdx new file mode 100644 index 000000000..2a23dea1a --- /dev/null +++ b/surfsense_web/content/docs/connectors/web-crawler.mdx @@ -0,0 +1,38 @@ +--- +title: Web Crawler +description: Crawl and index websites with SurfSense +--- + +# Web Crawler Connector + +Crawl and index public websites to make them searchable. + +## Prerequisites + +- Firecrawl API key (see [Prerequisites](/docs)) + +## Setup + +1. Navigate to your Search Space settings +2. Click on **Add Connector** +3. Select **Web Crawler** from the list +4. Enter the URL(s) you want to crawl +5. Configure crawl depth and settings + +## What Gets Indexed + +- Web page content +- Page titles and metadata +- Links and navigation +- Images and media (configurable) + +## Configuration Options + +- **Crawl Depth**: How many levels deep to crawl +- **Include/Exclude Patterns**: Filter which URLs to index +- **Rate Limiting**: Control crawl speed + +## Sync Frequency + +The Web Crawler connector supports scheduled re-crawling to keep your content up to date. + diff --git a/surfsense_web/content/docs/docker-installation.mdx b/surfsense_web/content/docs/docker-installation.mdx index 9957bbd37..d61aa3bc8 100644 --- a/surfsense_web/content/docs/docker-installation.mdx +++ b/surfsense_web/content/docs/docker-installation.mdx @@ -1,7 +1,6 @@ --- title: Docker Installation description: Setting up SurfSense using Docker -full: true --- diff --git a/surfsense_web/content/docs/index.mdx b/surfsense_web/content/docs/index.mdx index 2f0bd4f8f..42f863176 100644 --- a/surfsense_web/content/docs/index.mdx +++ b/surfsense_web/content/docs/index.mdx @@ -1,7 +1,6 @@ --- title: Prerequisites description: Required setup's before setting up SurfSense -full: true --- diff --git a/surfsense_web/content/docs/installation.mdx b/surfsense_web/content/docs/installation.mdx index 1a3f4553b..f5e948b64 100644 --- a/surfsense_web/content/docs/installation.mdx +++ b/surfsense_web/content/docs/installation.mdx @@ -1,7 +1,6 @@ --- title: Installation description: Current ways to use SurfSense -full: true --- # Installing SurfSense diff --git a/surfsense_web/content/docs/manual-installation.mdx b/surfsense_web/content/docs/manual-installation.mdx index e7caf93a6..3a0ee11e1 100644 --- a/surfsense_web/content/docs/manual-installation.mdx +++ b/surfsense_web/content/docs/manual-installation.mdx @@ -1,7 +1,6 @@ --- title: Manual Installation description: Setting up SurfSense manually for customized deployments (Preferred) -full: true --- # Manual Installation (Preferred) diff --git a/surfsense_web/content/docs/meta.json b/surfsense_web/content/docs/meta.json index c85ab5928..03790dd15 100644 --- a/surfsense_web/content/docs/meta.json +++ b/surfsense_web/content/docs/meta.json @@ -1,6 +1,13 @@ { - "title": "Setup", - "description": "The setup guide for Surfsense", + "title": "Documentation", + "description": "SurfSense Documentation", "root": true, - "pages": ["index", "installation", "docker-installation", "manual-installation"] + "pages": [ + "---Guides---", + "index", + "installation", + "docker-installation", + "manual-installation", + "connectors" + ] } From 5ebe708bd8d0bca213ded38424a2a0536215a4b8 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Tue, 6 Jan 2026 17:58:46 +0200 Subject: [PATCH 30/75] BE-1: Alembic migration to drop unique constraint for multiple connectors of same type per search space (idempotent) --- .../57_allow_multiple_connectors_per_type.py | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 surfsense_backend/alembic/versions/57_allow_multiple_connectors_per_type.py diff --git a/surfsense_backend/alembic/versions/57_allow_multiple_connectors_per_type.py b/surfsense_backend/alembic/versions/57_allow_multiple_connectors_per_type.py new file mode 100644 index 000000000..bd2fccf72 --- /dev/null +++ b/surfsense_backend/alembic/versions/57_allow_multiple_connectors_per_type.py @@ -0,0 +1,53 @@ +"""Allow multiple connectors of same type per search space + +Revision ID: 57 +Revises: 56 +Create Date: 2026-01-06 12:00:00.000000 + +""" + +from collections.abc import Sequence +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "57" +down_revision: str | None = "56" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None + +from sqlalchemy import text + +def upgrade() -> None: + connection = op.get_bind() + constraint_exists = connection.execute( + text(""" + SELECT 1 FROM information_schema.table_constraints + WHERE table_name='search_source_connectors' + AND constraint_type='UNIQUE' + AND constraint_name='uq_searchspace_user_connector_type' + """) + ).scalar() + if constraint_exists: + op.drop_constraint( + "uq_searchspace_user_connector_type", + "search_source_connectors", + type_="unique" + ) + +def downgrade() -> None: + connection = op.get_bind() + constraint_exists = connection.execute( + text(""" + SELECT 1 FROM information_schema.table_constraints + WHERE table_name='search_source_connectors' + AND constraint_type='UNIQUE' + AND constraint_name='uq_searchspace_user_connector_type' + """) + ).scalar() + if not constraint_exists: + op.create_unique_constraint( + "uq_searchspace_user_connector_type", + "search_source_connectors", + ["search_space_id", "user_id", "connector_type"] + ) + From 605edee033214683420a250504ace88cf3b08b9b Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Tue, 6 Jan 2026 21:37:49 +0530 Subject: [PATCH 31/75] feat: add onboarding tour component and integrate into dashboard layout - Introduced the OnboardingTour component to guide users through key features. - Integrated the OnboardingTour into the DashboardClientLayout for improved user experience. - Updated connector popup and sidebar navigation with data attributes for tour steps. --- .../[search_space_id]/client-layout.tsx | 2 + .../assistant-ui/connector-popup.tsx | 1 + surfsense_web/components/onboarding-tour.tsx | 523 ++++++++++++++++++ surfsense_web/components/sidebar/nav-main.tsx | 24 +- 4 files changed, 547 insertions(+), 3 deletions(-) create mode 100644 surfsense_web/components/onboarding-tour.tsx diff --git a/surfsense_web/app/dashboard/[search_space_id]/client-layout.tsx b/surfsense_web/app/dashboard/[search_space_id]/client-layout.tsx index 6e61ff7ac..c78cc7762 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/client-layout.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/client-layout.tsx @@ -17,6 +17,7 @@ import { activeSearchSpaceIdAtom } from "@/atoms/search-spaces/search-space-quer import { DocumentUploadDialogProvider } from "@/components/assistant-ui/document-upload-popup"; import { DashboardBreadcrumb } from "@/components/dashboard-breadcrumb"; import { LanguageSwitcher } from "@/components/LanguageSwitcher"; +import { OnboardingTour } from "@/components/onboarding-tour"; import { AppSidebarProvider } from "@/components/sidebar/AppSidebarProvider"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Separator } from "@/components/ui/separator"; @@ -242,6 +243,7 @@ export function DashboardClientLayout({ return ( + {/* Use AppSidebarProvider which fetches user, search space, and recent chats */} { return (

+ {/* Dark overlay with cutout using box-shadow technique */} +
+ {/* Blue shadow behind the button - starts from button border */} +
+ + ); +} + +function TourTooltip({ + step, + stepIndex, + totalSteps, + position, + targetRect, + onNext, + onPrev, + onSkip, + isDarkMode, +}: { + step: TourStep; + stepIndex: number; + totalSteps: number; + position: TooltipPosition; + targetRect: DOMRect; + onNext: () => void; + onPrev: () => void; + onSkip: () => void; + isDarkMode: boolean; +}) { + const isLastStep = stepIndex === totalSteps - 1; + const isFirstStep = stepIndex === 0; + + const bgColor = isDarkMode ? "#18181b" : "#18181b"; // Dark tooltip for both modes as shown in image + const textColor = "#ffffff"; + const mutedTextColor = "#a1a1aa"; + + // Calculate pointer line position + const getPointerStyles = (): React.CSSProperties => { + const lineLength = 16; + const dotSize = 6; + // Check if this is the documents step (stepIndex === 1) + const isDocumentsStep = stepIndex === 1; + + if (position.pointerPosition === "left") { + return { + position: "absolute", + left: -lineLength - dotSize, + top: isDocumentsStep ? "calc(50% - 8px)" : "50%", + transform: "translateY(-50%)", + display: "flex", + alignItems: "center", + }; + } + if (position.pointerPosition === "top") { + return { + position: "absolute", + top: -lineLength - dotSize, + left: "50%", + transform: "translateX(-50%)", + display: "flex", + flexDirection: "column", + alignItems: "center", + }; + } + return {}; + }; + + const renderPointer = () => { + const lineColor = "#18181B"; + + if (position.pointerPosition === "left") { + return ( +
+
+
+
+ ); + } + if (position.pointerPosition === "top") { + return ( +
+
+
+
+ ); + } + return null; + }; + + // Render step dots + const renderStepDots = () => { + return ( +
+ {Array.from({ length: totalSteps }).map((_, i) => ( +
+ ))} +
+ ); + }; + + return ( +
e.stopPropagation()} + onKeyDown={(e) => e.stopPropagation()} + > + {/* Pointer line */} + {renderPointer()} + +
+ {/* Content */} +
+

+ {step.title} +

+

+ {step.content} +

+
+ + {/* Footer */} +
+ {/* Step dots */} + {renderStepDots()} + + {/* Navigation buttons */} +
+ {!isFirstStep && ( + + )} + {isFirstStep && ( + + )} + +
+
+
+
+ ); +} + +export function OnboardingTour() { + const [isActive, setIsActive] = useState(false); + const [stepIndex, setStepIndex] = useState(0); + const [targetEl, setTargetEl] = useState(null); + const [position, setPosition] = useState(null); + const [targetRect, setTargetRect] = useState(null); + const [mounted, setMounted] = useState(false); + const { resolvedTheme } = useTheme(); + const retryCountRef = useRef(0); + const maxRetries = 10; + + const isDarkMode = resolvedTheme === "dark"; + const currentStep = TOUR_STEPS[stepIndex]; + + // Handle mounting for portal + useEffect(() => { + setMounted(true); + }, []); + + // Find and track target element with retry logic + const updateTarget = useCallback(() => { + if (!currentStep) return; + + const el = document.querySelector(currentStep.target); + if (el) { + setTargetEl(el); + setTargetRect(el.getBoundingClientRect()); + setPosition(calculatePosition(el, currentStep.placement)); + retryCountRef.current = 0; + } else if (retryCountRef.current < maxRetries) { + retryCountRef.current++; + setTimeout(() => { + const retryEl = document.querySelector(currentStep.target); + if (retryEl) { + setTargetEl(retryEl); + setTargetRect(retryEl.getBoundingClientRect()); + setPosition(calculatePosition(retryEl, currentStep.placement)); + retryCountRef.current = 0; + } + }, 200); + } + }, [currentStep]); + + // Start tour and find first target + useEffect(() => { + const timer = setTimeout(() => { + const el = document.querySelector(TOUR_STEPS[0].target); + if (el) { + setIsActive(true); + setTargetEl(el); + setTargetRect(el.getBoundingClientRect()); + setPosition(calculatePosition(el, TOUR_STEPS[0].placement)); + } + }, 1000); + + return () => clearTimeout(timer); + }, []); + + // Update position on resize/scroll + useEffect(() => { + if (!isActive || !targetEl) return; + + const handleUpdate = () => { + const rect = targetEl.getBoundingClientRect(); + if (rect.width > 0 && rect.height > 0) { + setTargetRect(rect); + setPosition(calculatePosition(targetEl, currentStep?.placement || "bottom")); + } + }; + + window.addEventListener("resize", handleUpdate); + window.addEventListener("scroll", handleUpdate, true); + + return () => { + window.removeEventListener("resize", handleUpdate); + window.removeEventListener("scroll", handleUpdate, true); + }; + }, [isActive, targetEl, currentStep?.placement]); + + // Update target when step changes + useEffect(() => { + if (isActive && currentStep) { + const timer = setTimeout(() => { + updateTarget(); + }, 100); + return () => clearTimeout(timer); + } + }, [isActive, updateTarget, currentStep]); + + // Ensure target element is above overlay layers so content is fully visible + useEffect(() => { + if (!targetEl || !isActive) return; + + const originalZIndex = (targetEl as HTMLElement).style.zIndex; + const originalPosition = (targetEl as HTMLElement).style.position; + + // Ensure the element has a position that allows z-index + if (getComputedStyle(targetEl).position === "static") { + (targetEl as HTMLElement).style.position = "relative"; + } + (targetEl as HTMLElement).style.zIndex = "99999"; + + return () => { + (targetEl as HTMLElement).style.zIndex = originalZIndex; + if (originalPosition) { + (targetEl as HTMLElement).style.position = originalPosition; + } else if (getComputedStyle(targetEl).position === "relative" && originalPosition === "") { + (targetEl as HTMLElement).style.position = ""; + } + }; + }, [targetEl, isActive]); + + const handleNext = useCallback(() => { + if (stepIndex < TOUR_STEPS.length - 1) { + retryCountRef.current = 0; + setStepIndex(stepIndex + 1); + } else { + setIsActive(false); + } + }, [stepIndex]); + + const handlePrev = useCallback(() => { + if (stepIndex > 0) { + retryCountRef.current = 0; + setStepIndex(stepIndex - 1); + } + }, [stepIndex]); + + const handleSkip = useCallback(() => { + setIsActive(false); + }, []); + + // Handle overlay click to close + const handleOverlayClick = useCallback(() => { + setIsActive(false); + }, []); + + // Handle escape key + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === "Escape" && isActive) { + setIsActive(false); + } + }; + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, [isActive]); + + // Don't render if not active or not mounted + if (!mounted || !isActive || !targetEl || !position || !currentStep || !targetRect) { + return null; + } + + return createPortal( +
+ {/* Clickable backdrop to close */} +
, + document.body + ); +} diff --git a/surfsense_web/components/sidebar/nav-main.tsx b/surfsense_web/components/sidebar/nav-main.tsx index 43c551875..a0dbe912f 100644 --- a/surfsense_web/components/sidebar/nav-main.tsx +++ b/surfsense_web/components/sidebar/nav-main.tsx @@ -131,7 +131,11 @@ export function NavMain({ items }: NavMainProps) { isActive={isActive} aria-label={`${translatedTitle} with submenu`} > - @@ -152,10 +156,18 @@ export function NavMain({ items }: NavMainProps) { {item.items?.map((subItem, subIndex) => { const translatedSubTitle = translateTitle(subItem.title); + const isDocumentsLink = + subItem.title === "Manage Documents" || + translatedSubTitle.toLowerCase().includes("documents"); return ( - + {translatedSubTitle} @@ -173,7 +185,13 @@ export function NavMain({ items }: NavMainProps) { isActive={isActive} aria-label={translatedTitle} > - + {translatedTitle} From 9f75a3f0b383f1e83d4dae7b5e2ea5e9faf9c526 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Tue, 6 Jan 2026 18:32:18 +0200 Subject: [PATCH 32/75] BE-1: Add connector_naming.py utilities for friendly auto-naming and unique identifier extraction --- .../app/utils/connector_naming.py | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 surfsense_backend/app/utils/connector_naming.py diff --git a/surfsense_backend/app/utils/connector_naming.py b/surfsense_backend/app/utils/connector_naming.py new file mode 100644 index 000000000..5081687ac --- /dev/null +++ b/surfsense_backend/app/utils/connector_naming.py @@ -0,0 +1,60 @@ +from app.db import SearchSourceConnectorType + +# Friendly display names for connector types +BASE_NAME_FOR_TYPE = { + SearchSourceConnectorType.GOOGLE_GMAIL_CONNECTOR: "Gmail", + SearchSourceConnectorType.GOOGLE_DRIVE_CONNECTOR: "Google Drive", + SearchSourceConnectorType.GOOGLE_CALENDAR_CONNECTOR: "Google Calendar", + SearchSourceConnectorType.SLACK_CONNECTOR: "Slack", + SearchSourceConnectorType.NOTION_CONNECTOR: "Notion", + SearchSourceConnectorType.GITHUB_CONNECTOR: "GitHub", + SearchSourceConnectorType.LINEAR_CONNECTOR: "Linear", + SearchSourceConnectorType.JIRA_CONNECTOR: "Jira", + SearchSourceConnectorType.DISCORD_CONNECTOR: "Discord", + SearchSourceConnectorType.CONFLUENCE_CONNECTOR: "Confluence", + SearchSourceConnectorType.AIRTABLE_CONNECTOR: "Airtable", + SearchSourceConnectorType.LUMA_CONNECTOR: "Luma", + # Add other connectors as needed, fallback below +} + +def get_base_name_for_type(connector_type: SearchSourceConnectorType) -> str: + return BASE_NAME_FOR_TYPE.get(connector_type, connector_type.replace("_", " ").title()) + + +def generate_unique_connector_name(connector_type: SearchSourceConnectorType, identifier: str | None) -> str: + base = get_base_name_for_type(connector_type) + if identifier: + return f"{base} - {identifier}" + return base + + +def extract_email_from_credentials(connector_type: SearchSourceConnectorType, credentials: dict) -> str | None: + if connector_type == SearchSourceConnectorType.GOOGLE_GMAIL_CONNECTOR: + return credentials.get("email") or credentials.get("user_email") + if connector_type == SearchSourceConnectorType.GOOGLE_DRIVE_CONNECTOR: + return credentials.get("email") + if connector_type == SearchSourceConnectorType.GOOGLE_CALENDAR_CONNECTOR: + return credentials.get("email") + if connector_type == SearchSourceConnectorType.SLACK_CONNECTOR: + return credentials.get("team_name") or credentials.get("team_id") + if connector_type == SearchSourceConnectorType.NOTION_CONNECTOR: + return credentials.get("workspace_name") + if connector_type == SearchSourceConnectorType.GITHUB_CONNECTOR: + return credentials.get("username") + if connector_type == SearchSourceConnectorType.LINEAR_CONNECTOR: + return credentials.get("workspace_name") + if connector_type == SearchSourceConnectorType.JIRA_CONNECTOR: + return credentials.get("base_url") or credentials.get("cloud_id") + if connector_type == SearchSourceConnectorType.CONFLUENCE_CONNECTOR: + return credentials.get("base_url") or credentials.get("cloud_id") + if connector_type == SearchSourceConnectorType.DISCORD_CONNECTOR: + return credentials.get("guild_name") + if connector_type == SearchSourceConnectorType.AIRTABLE_CONNECTOR: + return credentials.get("base_name") + if connector_type == SearchSourceConnectorType.LUMA_CONNECTOR: + return credentials.get("account_name") + for key in ["email", "username", "workspace_name", "team_name", "base_url", "guild_name", "site_name", "account_name"]: + if credentials.get(key): + return credentials.get(key) + return None + From 21d45b8b2139b49f560d741b29e8df08aab70a51 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Tue, 6 Jan 2026 18:41:14 +0200 Subject: [PATCH 33/75] BE-1: Allow multiple connectors of same type per search space (remove duplicate checks, update docstrings) --- .../routes/search_source_connectors_routes.py | 20 +++---------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/surfsense_backend/app/routes/search_source_connectors_routes.py b/surfsense_backend/app/routes/search_source_connectors_routes.py index d6fdedd7c..a92be5f6e 100644 --- a/surfsense_backend/app/routes/search_source_connectors_routes.py +++ b/surfsense_backend/app/routes/search_source_connectors_routes.py @@ -7,7 +7,7 @@ PUT /search-source-connectors/{connector_id} - Update a specific connector DELETE /search-source-connectors/{connector_id} - Delete a specific connector POST /search-source-connectors/{connector_id}/index - Index content from a connector to a search space -Note: Each search space can have only one connector of each type per user (based on search_space_id, user_id, and connector_type). +Note: Each search space can have multiple connectors of the same type per user (uniqueness is no longer enforced, you may connect several accounts of the same type). """ import logging @@ -111,7 +111,7 @@ async def create_search_source_connector( Create a new search source connector. Requires CONNECTORS_CREATE permission. - Each search space can have only one connector of each type (based on search_space_id and connector_type). + Each search space can have multiple connectors of the same type (e.g., multiple Gmail, Slack, etc. accounts). The config must contain the appropriate keys for the connector type. """ try: @@ -124,20 +124,6 @@ async def create_search_source_connector( "You don't have permission to create connectors in this search space", ) - # Check if a connector with the same type already exists for this search space - result = await session.execute( - select(SearchSourceConnector).filter( - SearchSourceConnector.search_space_id == search_space_id, - SearchSourceConnector.connector_type == connector.connector_type, - ) - ) - existing_connector = result.scalars().first() - if existing_connector: - raise HTTPException( - status_code=409, - detail=f"A connector with type {connector.connector_type} already exists in this search space.", - ) - # Prepare connector data connector_data = connector.model_dump() @@ -183,7 +169,7 @@ async def create_search_source_connector( await session.rollback() raise HTTPException( status_code=409, - detail=f"Integrity error: A connector with this type already exists in this search space. {e!s}", + detail=f"Integrity error: {e!s}", ) from e except HTTPException: await session.rollback() From e46a0e0a9545280460445a52be4547167f565cf2 Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Tue, 6 Jan 2026 22:14:11 +0530 Subject: [PATCH 34/75] fix: improve onboarding tour functionality and UI - Updated onboarding tour messages to include Gmail and Drive for better clarity. - Refactored Spotlight component to enhance element targeting and prevent flickering. - Optimized rendering logic for Spotlight and TourTooltip to ensure they only display when target data is available. --- surfsense_web/components/onboarding-tour.tsx | 70 +++++++++++++------- 1 file changed, 46 insertions(+), 24 deletions(-) diff --git a/surfsense_web/components/onboarding-tour.tsx b/surfsense_web/components/onboarding-tour.tsx index 077a93be3..1e7be0f44 100644 --- a/surfsense_web/components/onboarding-tour.tsx +++ b/surfsense_web/components/onboarding-tour.tsx @@ -16,7 +16,7 @@ const TOUR_STEPS: TourStep[] = [ target: '[data-joyride="connector-icon"]', title: "Connect your data sources", content: - "Click this icon to connect and sync data from GitHub, Slack, Notion, Jira, Confluence, and more.", + "Connect and sync data from Gmail, Drive, Slack, Notion, Jira, Confluence, and more.", placement: "bottom", }, { @@ -78,18 +78,21 @@ function calculatePosition(targetEl: Element, placement: TourStep["placement"]): function Spotlight({ targetEl, isDarkMode, - stepIndex, + currentStepTarget, }: { targetEl: Element; isDarkMode: boolean; - stepIndex: number; + currentStepTarget: string; }) { const rect = targetEl.getBoundingClientRect(); const padding = 6; const shadowColor = isDarkMode ? "#172554" : "#0c1a3a"; - // Check if this is the connector icon step (stepIndex === 0) - const isConnectorStep = stepIndex === 0; + // Check if this is the connector icon step - verify both the selector matches AND the element matches + // This prevents the shape from changing before targetEl updates + const isConnectorSelector = currentStepTarget === '[data-joyride="connector-icon"]'; + const isConnectorElement = targetEl.matches('[data-joyride="connector-icon"]'); + const isConnectorStep = isConnectorSelector && isConnectorElement; // For circle, use the larger dimension to ensure it's a perfect circle const circleSize = isConnectorStep ? Math.max(rect.width, rect.height) : 0; @@ -135,7 +138,6 @@ function TourTooltip({ stepIndex, totalSteps, position, - targetRect, onNext, onPrev, onSkip, @@ -243,7 +245,7 @@ function TourTooltip({
{Array.from({ length: totalSteps }).map((_, i) => (
{ if (isActive && currentStep) { - const timer = setTimeout(() => { - updateTarget(); - }, 100); - return () => clearTimeout(timer); + // Try to find element synchronously first to prevent any delay + const el = document.querySelector(currentStep.target); + if (el) { + // Found immediately - update state synchronously to prevent flicker + const rect = el.getBoundingClientRect(); + const newPosition = calculatePosition(el, currentStep.placement); + // React 18+ automatically batches these updates + setTargetEl(el); + setTargetRect(rect); + setPosition(newPosition); + retryCountRef.current = 0; + } else { + // Not found immediately, use updateTarget with retry logic + // Use requestAnimationFrame to batch with next paint + const frameId = requestAnimationFrame(() => { + updateTarget(); + }); + return () => cancelAnimationFrame(frameId); + } } }, [isActive, updateTarget, currentStep]); @@ -492,7 +509,7 @@ export function OnboardingTour() { }, [isActive]); // Don't render if not active or not mounted - if (!mounted || !isActive || !targetEl || !position || !currentStep || !targetRect) { + if (!mounted || !isActive) { return null; } @@ -505,18 +522,23 @@ export function OnboardingTour() { onClick={handleOverlayClick} aria-label="Close tour" /> - - + {/* Only render Spotlight and TourTooltip when we have target data */} + {targetEl && position && currentStep && targetRect && ( + <> + + + + )}
, document.body ); From d7b8890e9ec920738f7e985ce1a68e986008cc90 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Tue, 6 Jan 2026 18:55:35 +0200 Subject: [PATCH 35/75] BE-2: Remove duplicate checks and auto-generate user-friendly names for Google connector OAuth callbacks (consistent comments, identifier extraction) --- .../google_calendar_add_connector_route.py | 27 ++++++++---------- .../google_drive_add_connector_route.py | 28 +++++++------------ .../google_gmail_add_connector_route.py | 26 +++++++---------- .../app/utils/connector_naming.py | 2 +- 4 files changed, 32 insertions(+), 51 deletions(-) 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 6c6ae4e40..73d50cb7e 100644 --- a/surfsense_backend/app/routes/google_calendar_add_connector_route.py +++ b/surfsense_backend/app/routes/google_calendar_add_connector_route.py @@ -23,6 +23,7 @@ from app.db import ( ) from app.users import current_active_user from app.utils.oauth_security import OAuthStateManager, TokenEncryption +from app.utils.connector_naming import generate_unique_connector_name, extract_identifier_from_credentials logger = logging.getLogger(__name__) @@ -191,23 +192,17 @@ async def calendar_callback( creds_dict["_token_encrypted"] = True try: - # Check if a connector with the same type already exists for this search space and user - result = await session.execute( - select(SearchSourceConnector).filter( - SearchSourceConnector.search_space_id == space_id, - SearchSourceConnector.user_id == user_id, - SearchSourceConnector.connector_type - == SearchSourceConnectorType.GOOGLE_CALENDAR_CONNECTOR, - ) + + # Extract unique identifier from connector credentials + connector_identifier = extract_identifier_from_credentials( + SearchSourceConnectorType.GOOGLE_CALENDAR_CONNECTOR, creds_dict + ) + # Generate a unique, user-friendly connector name from credentials/account info + connector_name = generate_unique_connector_name( + SearchSourceConnectorType.GOOGLE_CALENDAR_CONNECTOR, connector_identifier ) - existing_connector = result.scalars().first() - if existing_connector: - raise HTTPException( - status_code=409, - detail="A GOOGLE_CALENDAR_CONNECTOR connector already exists in this search space. Each search space can have only one connector of each type per user.", - ) db_connector = SearchSourceConnector( - name="Google Calendar Connector", + name=connector_name, connector_type=SearchSourceConnectorType.GOOGLE_CALENDAR_CONNECTOR, config=creds_dict, search_space_id=space_id, @@ -231,7 +226,7 @@ async def calendar_callback( await session.rollback() raise HTTPException( status_code=409, - detail=f"Integrity error: A connector with this type already exists. {e!s}", + detail=f"Database integrity error: {e!s}", ) from e except HTTPException: await session.rollback() diff --git a/surfsense_backend/app/routes/google_drive_add_connector_route.py b/surfsense_backend/app/routes/google_drive_add_connector_route.py index 6caf3f204..3e9800ed1 100644 --- a/surfsense_backend/app/routes/google_drive_add_connector_route.py +++ b/surfsense_backend/app/routes/google_drive_add_connector_route.py @@ -37,6 +37,7 @@ from app.db import ( ) from app.users import current_active_user from app.utils.oauth_security import OAuthStateManager, TokenEncryption +from app.utils.connector_naming import generate_unique_connector_name, extract_identifier_from_credentials # Relax token scope validation for Google OAuth os.environ["OAUTHLIB_RELAX_TOKEN_SCOPE"] = "1" @@ -245,26 +246,17 @@ async def drive_callback( # Mark that credentials are encrypted for backward compatibility creds_dict["_token_encrypted"] = True - # Check if connector already exists for this space/user - result = await session.execute( - select(SearchSourceConnector).filter( - SearchSourceConnector.search_space_id == space_id, - SearchSourceConnector.user_id == user_id, - SearchSourceConnector.connector_type - == SearchSourceConnectorType.GOOGLE_DRIVE_CONNECTOR, - ) + # Extract unique identifier from connector credentials + connector_identifier = extract_identifier_from_credentials( + SearchSourceConnectorType.GOOGLE_DRIVE_CONNECTOR, creds_dict + ) + # Generate a unique, user-friendly connector name from credentials/account info + connector_name = generate_unique_connector_name( + SearchSourceConnectorType.GOOGLE_DRIVE_CONNECTOR, connector_identifier ) - existing_connector = result.scalars().first() - if existing_connector: - raise HTTPException( - status_code=409, - detail="A GOOGLE_DRIVE_CONNECTOR already exists in this search space. Each search space can have only one connector of each type per user.", - ) - - # Create new connector (NO folder selection here - happens at index time) db_connector = SearchSourceConnector( - name="Google Drive Connector", + name=connector_name, connector_type=SearchSourceConnectorType.GOOGLE_DRIVE_CONNECTOR, config={ **creds_dict, @@ -318,7 +310,7 @@ async def drive_callback( logger.error(f"Database integrity error: {e!s}", exc_info=True) raise HTTPException( status_code=409, - detail="A connector with this configuration already exists.", + detail=f"Database integrity error: {e!s}", ) from e except Exception as e: await session.rollback() diff --git a/surfsense_backend/app/routes/google_gmail_add_connector_route.py b/surfsense_backend/app/routes/google_gmail_add_connector_route.py index 20a51c1a1..01bca39f4 100644 --- a/surfsense_backend/app/routes/google_gmail_add_connector_route.py +++ b/surfsense_backend/app/routes/google_gmail_add_connector_route.py @@ -23,6 +23,7 @@ from app.db import ( ) from app.users import current_active_user from app.utils.oauth_security import OAuthStateManager, TokenEncryption +from app.utils.connector_naming import generate_unique_connector_name, extract_identifier_from_credentials logger = logging.getLogger(__name__) @@ -222,23 +223,16 @@ async def gmail_callback( creds_dict["_token_encrypted"] = True try: - # Check if a connector with the same type already exists for this search space and user - result = await session.execute( - select(SearchSourceConnector).filter( - SearchSourceConnector.search_space_id == space_id, - SearchSourceConnector.user_id == user_id, - SearchSourceConnector.connector_type - == SearchSourceConnectorType.GOOGLE_GMAIL_CONNECTOR, - ) + # Extract unique identifier from connector credentials + connector_identifier = extract_identifier_from_credentials( + SearchSourceConnectorType.GOOGLE_GMAIL_CONNECTOR, creds_dict + ) + # Generate a unique, user-friendly connector name from credentials/account info + connector_name = generate_unique_connector_name( + SearchSourceConnectorType.GOOGLE_GMAIL_CONNECTOR, connector_identifier ) - existing_connector = result.scalars().first() - if existing_connector: - raise HTTPException( - status_code=409, - detail="A GOOGLE_GMAIL_CONNECTOR connector already exists in this search space. Each search space can have only one connector of each type per user.", - ) db_connector = SearchSourceConnector( - name="Google Gmail Connector", + name=connector_name, connector_type=SearchSourceConnectorType.GOOGLE_GMAIL_CONNECTOR, config=creds_dict, search_space_id=space_id, @@ -264,7 +258,7 @@ async def gmail_callback( logger.error(f"Database integrity error: {e!s}") raise HTTPException( status_code=409, - detail="A connector with this configuration already exists.", + detail=f"Database integrity error: {e!s}", ) from e except ValidationError as e: await session.rollback() diff --git a/surfsense_backend/app/utils/connector_naming.py b/surfsense_backend/app/utils/connector_naming.py index 5081687ac..16c6d8f1e 100644 --- a/surfsense_backend/app/utils/connector_naming.py +++ b/surfsense_backend/app/utils/connector_naming.py @@ -28,7 +28,7 @@ def generate_unique_connector_name(connector_type: SearchSourceConnectorType, id return base -def extract_email_from_credentials(connector_type: SearchSourceConnectorType, credentials: dict) -> str | None: +def extract_identifier_from_credentials(connector_type: SearchSourceConnectorType, credentials: dict) -> str | None: if connector_type == SearchSourceConnectorType.GOOGLE_GMAIL_CONNECTOR: return credentials.get("email") or credentials.get("user_email") if connector_type == SearchSourceConnectorType.GOOGLE_DRIVE_CONNECTOR: From 7900d6acc029f6900bfa97f8aa01eb5972dd99c3 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Tue, 6 Jan 2026 19:05:22 +0200 Subject: [PATCH 36/75] BE-2: Remove duplicate checks and enable auto-generation of user-friendly names for Slack & Notion OAuth connectors --- .../app/routes/notion_add_connector_route.py | 55 ++++++++----------- .../app/routes/slack_add_connector_route.py | 54 ++++++++---------- 2 files changed, 44 insertions(+), 65 deletions(-) diff --git a/surfsense_backend/app/routes/notion_add_connector_route.py b/surfsense_backend/app/routes/notion_add_connector_route.py index 462ac398c..832ca4abc 100644 --- a/surfsense_backend/app/routes/notion_add_connector_route.py +++ b/surfsense_backend/app/routes/notion_add_connector_route.py @@ -26,6 +26,7 @@ from app.db import ( from app.schemas.notion_auth_credentials import NotionAuthCredentialsBase from app.users import current_active_user from app.utils.oauth_security import OAuthStateManager, TokenEncryption +from app.utils.connector_naming import generate_unique_connector_name, extract_identifier_from_credentials logger = logging.getLogger(__name__) @@ -262,39 +263,27 @@ async def notion_callback( "_token_encrypted": True, } - # Check if connector already exists for this search space and user - existing_connector_result = await session.execute( - select(SearchSourceConnector).filter( - SearchSourceConnector.search_space_id == space_id, - SearchSourceConnector.user_id == user_id, - SearchSourceConnector.connector_type - == SearchSourceConnectorType.NOTION_CONNECTOR, - ) + # Extract unique identifier from connector credentials + connector_identifier = extract_identifier_from_credentials( + SearchSourceConnectorType.NOTION_CONNECTOR, connector_config + ) + # Generate a unique, user-friendly connector name from credentials/account info + connector_name = generate_unique_connector_name( + SearchSourceConnectorType.NOTION_CONNECTOR, connector_identifier + ) + # Create new connector + new_connector = SearchSourceConnector( + name=connector_name, + connector_type=SearchSourceConnectorType.NOTION_CONNECTOR, + is_indexable=True, + config=connector_config, + search_space_id=space_id, + user_id=user_id, + ) + session.add(new_connector) + logger.info( + f"Created new Notion connector for user {user_id} in space {space_id}" ) - existing_connector = existing_connector_result.scalars().first() - - if existing_connector: - # Update existing connector - existing_connector.config = connector_config - existing_connector.name = "Notion Connector" - existing_connector.is_indexable = True - logger.info( - f"Updated existing Notion connector for user {user_id} in space {space_id}" - ) - else: - # Create new connector - new_connector = SearchSourceConnector( - name="Notion Connector", - connector_type=SearchSourceConnectorType.NOTION_CONNECTOR, - is_indexable=True, - config=connector_config, - search_space_id=space_id, - user_id=user_id, - ) - session.add(new_connector) - logger.info( - f"Created new Notion connector for user {user_id} in space {space_id}" - ) try: await session.commit() @@ -314,7 +303,7 @@ async def notion_callback( await session.rollback() raise HTTPException( status_code=409, - detail=f"Integrity error: A connector with this type already exists. {e!s}", + detail=f"Database integrity error: {e!s}", ) from e except Exception as e: logger.error(f"Failed to create search source connector: {e!s}") diff --git a/surfsense_backend/app/routes/slack_add_connector_route.py b/surfsense_backend/app/routes/slack_add_connector_route.py index 71a362119..c0693f16f 100644 --- a/surfsense_backend/app/routes/slack_add_connector_route.py +++ b/surfsense_backend/app/routes/slack_add_connector_route.py @@ -26,6 +26,7 @@ from app.db import ( from app.schemas.slack_auth_credentials import SlackAuthCredentialsBase from app.users import current_active_user from app.utils.oauth_security import OAuthStateManager, TokenEncryption +from app.utils.connector_naming import generate_unique_connector_name, extract_identifier_from_credentials logger = logging.getLogger(__name__) @@ -272,39 +273,28 @@ async def slack_callback( "_token_encrypted": True, } - # Check if connector already exists for this search space and user - existing_connector_result = await session.execute( - select(SearchSourceConnector).filter( - SearchSourceConnector.search_space_id == space_id, - SearchSourceConnector.user_id == user_id, - SearchSourceConnector.connector_type - == SearchSourceConnectorType.SLACK_CONNECTOR, - ) + # Extract unique identifier from connector credentials + connector_identifier = extract_identifier_from_credentials( + SearchSourceConnectorType.SLACK_CONNECTOR, connector_config + ) + # Generate a unique, user-friendly connector name from credentials/account info + connector_name = generate_unique_connector_name( + SearchSourceConnectorType.SLACK_CONNECTOR, connector_identifier ) - existing_connector = existing_connector_result.scalars().first() - if existing_connector: - # Update existing connector - existing_connector.config = connector_config - existing_connector.name = "Slack Connector" - existing_connector.is_indexable = True - logger.info( - f"Updated existing Slack connector for user {user_id} in space {space_id}" - ) - else: - # Create new connector - new_connector = SearchSourceConnector( - name="Slack Connector", - connector_type=SearchSourceConnectorType.SLACK_CONNECTOR, - is_indexable=True, - config=connector_config, - search_space_id=space_id, - user_id=user_id, - ) - session.add(new_connector) - logger.info( - f"Created new Slack connector for user {user_id} in space {space_id}" - ) + # Create new connector + new_connector = SearchSourceConnector( + name=connector_name, + connector_type=SearchSourceConnectorType.SLACK_CONNECTOR, + is_indexable=True, + config=connector_config, + search_space_id=space_id, + user_id=user_id, + ) + session.add(new_connector) + logger.info( + f"Created new Slack connector for user {user_id} in space {space_id}" + ) try: await session.commit() @@ -324,7 +314,7 @@ async def slack_callback( await session.rollback() raise HTTPException( status_code=409, - detail=f"Integrity error: A connector with this type already exists. {e!s}", + detail=f"Database integrity error: {e!s}", ) from e except Exception as e: logger.error(f"Failed to create search source connector: {e!s}") From c58a3fba55654442d245a4a5a94acb877113a96c Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Tue, 6 Jan 2026 19:12:18 +0200 Subject: [PATCH 37/75] BE-2: Remove duplicate logic and enable auto-friendly naming for Linear, Jira, and Discord connector OAuth callbacks --- .../app/routes/discord_add_connector_route.py | 55 ++++++++----------- .../app/routes/jira_add_connector_route.py | 55 ++++++++----------- .../app/routes/linear_add_connector_route.py | 55 ++++++++----------- 3 files changed, 66 insertions(+), 99 deletions(-) diff --git a/surfsense_backend/app/routes/discord_add_connector_route.py b/surfsense_backend/app/routes/discord_add_connector_route.py index 6bebac718..73092f143 100644 --- a/surfsense_backend/app/routes/discord_add_connector_route.py +++ b/surfsense_backend/app/routes/discord_add_connector_route.py @@ -26,6 +26,7 @@ from app.db import ( from app.schemas.discord_auth_credentials import DiscordAuthCredentialsBase from app.users import current_active_user from app.utils.oauth_security import OAuthStateManager, TokenEncryption +from app.utils.connector_naming import generate_unique_connector_name, extract_identifier_from_credentials logger = logging.getLogger(__name__) @@ -284,39 +285,27 @@ async def discord_callback( "_token_encrypted": True, } - # Check if connector already exists for this search space and user - existing_connector_result = await session.execute( - select(SearchSourceConnector).filter( - SearchSourceConnector.search_space_id == space_id, - SearchSourceConnector.user_id == user_id, - SearchSourceConnector.connector_type - == SearchSourceConnectorType.DISCORD_CONNECTOR, - ) + # Extract unique identifier from connector credentials + connector_identifier = extract_identifier_from_credentials( + SearchSourceConnectorType.DISCORD_CONNECTOR, connector_config + ) + # Generate a unique, user-friendly connector name from credentials/account info + connector_name = generate_unique_connector_name( + SearchSourceConnectorType.DISCORD_CONNECTOR, connector_identifier + ) + # Create new connector + new_connector = SearchSourceConnector( + name=connector_name, + connector_type=SearchSourceConnectorType.DISCORD_CONNECTOR, + is_indexable=True, + config=connector_config, + search_space_id=space_id, + user_id=user_id, + ) + session.add(new_connector) + logger.info( + f"Created new Discord connector for user {user_id} in space {space_id}" ) - existing_connector = existing_connector_result.scalars().first() - - if existing_connector: - # Update existing connector - existing_connector.config = connector_config - existing_connector.name = "Discord Connector" - existing_connector.is_indexable = True - logger.info( - f"Updated existing Discord connector for user {user_id} in space {space_id}" - ) - else: - # Create new connector - new_connector = SearchSourceConnector( - name="Discord Connector", - connector_type=SearchSourceConnectorType.DISCORD_CONNECTOR, - is_indexable=True, - config=connector_config, - search_space_id=space_id, - user_id=user_id, - ) - session.add(new_connector) - logger.info( - f"Created new Discord connector for user {user_id} in space {space_id}" - ) try: await session.commit() @@ -336,7 +325,7 @@ async def discord_callback( await session.rollback() raise HTTPException( status_code=409, - detail=f"Integrity error: A connector with this type already exists. {e!s}", + detail=f"Database integrity error: {e!s}", ) from e except Exception as e: logger.error(f"Failed to create search source connector: {e!s}") diff --git a/surfsense_backend/app/routes/jira_add_connector_route.py b/surfsense_backend/app/routes/jira_add_connector_route.py index 740c30300..0d662f095 100644 --- a/surfsense_backend/app/routes/jira_add_connector_route.py +++ b/surfsense_backend/app/routes/jira_add_connector_route.py @@ -27,6 +27,7 @@ from app.db import ( from app.schemas.atlassian_auth_credentials import AtlassianAuthCredentialsBase from app.users import current_active_user from app.utils.oauth_security import OAuthStateManager, TokenEncryption +from app.utils.connector_naming import generate_unique_connector_name, extract_identifier_from_credentials logger = logging.getLogger(__name__) @@ -306,39 +307,27 @@ async def jira_callback( "_token_encrypted": True, } - # Check if connector already exists for this search space and user - existing_connector_result = await session.execute( - select(SearchSourceConnector).filter( - SearchSourceConnector.search_space_id == space_id, - SearchSourceConnector.user_id == user_id, - SearchSourceConnector.connector_type - == SearchSourceConnectorType.JIRA_CONNECTOR, - ) + # Extract unique identifier from connector credentials + connector_identifier = extract_identifier_from_credentials( + SearchSourceConnectorType.JIRA_CONNECTOR, connector_config + ) + # Generate a unique, user-friendly connector name from credentials/account info + connector_name = generate_unique_connector_name( + SearchSourceConnectorType.JIRA_CONNECTOR, connector_identifier + ) + # Create new connector + new_connector = SearchSourceConnector( + name=connector_name, + connector_type=SearchSourceConnectorType.JIRA_CONNECTOR, + is_indexable=True, + config=connector_config, + search_space_id=space_id, + user_id=user_id, + ) + session.add(new_connector) + logger.info( + f"Created new Jira connector for user {user_id} in space {space_id}" ) - existing_connector = existing_connector_result.scalars().first() - - if existing_connector: - # Update existing connector - existing_connector.config = connector_config - existing_connector.name = "Jira Connector" - existing_connector.is_indexable = True - logger.info( - f"Updated existing Jira connector for user {user_id} in space {space_id}" - ) - else: - # Create new connector - new_connector = SearchSourceConnector( - name="Jira Connector", - connector_type=SearchSourceConnectorType.JIRA_CONNECTOR, - is_indexable=True, - config=connector_config, - search_space_id=space_id, - user_id=user_id, - ) - session.add(new_connector) - logger.info( - f"Created new Jira connector for user {user_id} in space {space_id}" - ) try: await session.commit() @@ -358,7 +347,7 @@ async def jira_callback( await session.rollback() raise HTTPException( status_code=409, - detail=f"Integrity error: A connector with this type already exists. {e!s}", + detail=f"Database integrity error: {e!s}", ) from e except Exception as e: logger.error(f"Failed to create search source connector: {e!s}") diff --git a/surfsense_backend/app/routes/linear_add_connector_route.py b/surfsense_backend/app/routes/linear_add_connector_route.py index 7a7fc196a..e13c2dc5f 100644 --- a/surfsense_backend/app/routes/linear_add_connector_route.py +++ b/surfsense_backend/app/routes/linear_add_connector_route.py @@ -26,6 +26,7 @@ from app.db import ( from app.schemas.linear_auth_credentials import LinearAuthCredentialsBase from app.users import current_active_user from app.utils.oauth_security import OAuthStateManager, TokenEncryption +from app.utils.connector_naming import generate_unique_connector_name, extract_identifier_from_credentials logger = logging.getLogger(__name__) @@ -260,39 +261,27 @@ async def linear_callback( "_token_encrypted": True, } - # Check if connector already exists for this search space and user - existing_connector_result = await session.execute( - select(SearchSourceConnector).filter( - SearchSourceConnector.search_space_id == space_id, - SearchSourceConnector.user_id == user_id, - SearchSourceConnector.connector_type - == SearchSourceConnectorType.LINEAR_CONNECTOR, - ) + # Extract unique identifier from connector credentials + connector_identifier = extract_identifier_from_credentials( + SearchSourceConnectorType.LINEAR_CONNECTOR, connector_config + ) + # Generate a unique, user-friendly connector name from credentials/account info + connector_name = generate_unique_connector_name( + SearchSourceConnectorType.LINEAR_CONNECTOR, connector_identifier + ) + # Create new connector + new_connector = SearchSourceConnector( + name=connector_name, + connector_type=SearchSourceConnectorType.LINEAR_CONNECTOR, + is_indexable=True, + config=connector_config, + search_space_id=space_id, + user_id=user_id, + ) + session.add(new_connector) + logger.info( + f"Created new Linear connector for user {user_id} in space {space_id}" ) - existing_connector = existing_connector_result.scalars().first() - - if existing_connector: - # Update existing connector - existing_connector.config = connector_config - existing_connector.name = "Linear Connector" - existing_connector.is_indexable = True - logger.info( - f"Updated existing Linear connector for user {user_id} in space {space_id}" - ) - else: - # Create new connector - new_connector = SearchSourceConnector( - name="Linear Connector", - connector_type=SearchSourceConnectorType.LINEAR_CONNECTOR, - is_indexable=True, - config=connector_config, - search_space_id=space_id, - user_id=user_id, - ) - session.add(new_connector) - logger.info( - f"Created new Linear connector for user {user_id} in space {space_id}" - ) try: await session.commit() @@ -312,7 +301,7 @@ async def linear_callback( await session.rollback() raise HTTPException( status_code=409, - detail=f"Integrity error: A connector with this type already exists. {e!s}", + detail=f"Database integrity error: {e!s}", ) from e except Exception as e: logger.error(f"Failed to create search source connector: {e!s}") From 0621304fbde9ceb97957db6d4e7c3e7c5813fad8 Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Tue, 6 Jan 2026 22:43:59 +0530 Subject: [PATCH 38/75] refactor: enhance onboarding tour UI and functionality - Updated tooltip and spotlight styles for improved visibility and animation. - Adjusted background and text colors based on dark mode settings for better user experience. - Introduced animation for tooltip content changes to enhance user engagement. - Refactored rendering logic to ensure spotlight updates sync with tooltip animations. --- surfsense_web/components/onboarding-tour.tsx | 151 ++++++++++++++----- 1 file changed, 113 insertions(+), 38 deletions(-) diff --git a/surfsense_web/components/onboarding-tour.tsx b/surfsense_web/components/onboarding-tour.tsx index 1e7be0f44..19d616237 100644 --- a/surfsense_web/components/onboarding-tour.tsx +++ b/surfsense_web/components/onboarding-tour.tsx @@ -15,8 +15,7 @@ const TOUR_STEPS: TourStep[] = [ { target: '[data-joyride="connector-icon"]', title: "Connect your data sources", - content: - "Connect and sync data from Gmail, Drive, Slack, Notion, Jira, Confluence, and more.", + content: "Connect and sync data from Gmail, Drive, Slack, Notion, Jira, Confluence, and more.", placement: "bottom", }, { @@ -86,7 +85,7 @@ function Spotlight({ }) { const rect = targetEl.getBoundingClientRect(); const padding = 6; - const shadowColor = isDarkMode ? "#172554" : "#0c1a3a"; + const shadowColor = isDarkMode ? "#172554" : "#3b82f6"; // Check if this is the connector icon step - verify both the selector matches AND the element matches // This prevents the shape from changing before targetEl updates @@ -110,7 +109,9 @@ function Spotlight({ width: isConnectorStep ? circleSize + padding * 2 : rect.width + padding * 2, height: isConnectorStep ? circleSize + padding * 2 : rect.height + padding * 2, borderRadius: isConnectorStep ? "50%" : 8, - boxShadow: `0 0 0 9999px rgba(0, 0, 0, 0.6)`, + boxShadow: isDarkMode + ? `0 0 0 9999px rgba(0, 0, 0, 0.6)` + : `0 0 0 9999px rgba(0, 0, 0, 0.3)`, backgroundColor: "transparent", zIndex: 99996, }} @@ -124,7 +125,9 @@ function Spotlight({ width: isConnectorStep ? circleSize : rect.width, height: isConnectorStep ? circleSize : rect.height, borderRadius: isConnectorStep ? "50%" : 8, - boxShadow: `0 0 10px 2px ${shadowColor}CC, 0 0 20px 6px ${shadowColor}99, 0 0 40px 12px ${shadowColor}66`, + boxShadow: isDarkMode + ? `0 0 10px 2px ${shadowColor}CC, 0 0 20px 6px ${shadowColor}99, 0 0 40px 12px ${shadowColor}66` + : `0 0 6px 1px ${shadowColor}80, 0 0 12px 3px ${shadowColor}50, 0 0 20px 6px ${shadowColor}30`, backgroundColor: "transparent", zIndex: 99997, }} @@ -153,12 +156,25 @@ function TourTooltip({ onSkip: () => void; isDarkMode: boolean; }) { + const [contentKey, setContentKey] = useState(stepIndex); + const [shouldAnimate, setShouldAnimate] = useState(false); + const prevStepIndexRef = useRef(stepIndex); const isLastStep = stepIndex === totalSteps - 1; const isFirstStep = stepIndex === 0; - const bgColor = isDarkMode ? "#18181b" : "#18181b"; // Dark tooltip for both modes as shown in image - const textColor = "#ffffff"; - const mutedTextColor = "#a1a1aa"; + // Update content key when step changes to trigger animation + // Only animate if stepIndex actually changes (not on initial mount) + useEffect(() => { + if (prevStepIndexRef.current !== stepIndex) { + setShouldAnimate(true); + setContentKey(stepIndex); + prevStepIndexRef.current = stepIndex; + } + }, [stepIndex]); + + const bgColor = isDarkMode ? "#18181b" : "#ffffff"; + const textColor = isDarkMode ? "#ffffff" : "#18181b"; + const mutedTextColor = isDarkMode ? "#a1a1aa" : "#71717a"; // Calculate pointer line position const getPointerStyles = (): React.CSSProperties => { @@ -192,7 +208,7 @@ function TourTooltip({ }; const renderPointer = () => { - const lineColor = "#18181B"; + const lineColor = isDarkMode ? "#18181B" : "#ffffff"; if (position.pointerPosition === "left") { return ( @@ -250,7 +266,14 @@ function TourTooltip({ width: 6, height: 6, borderRadius: "50%", - backgroundColor: i === stepIndex ? "#ffffff" : "#52525b", + backgroundColor: + i === stepIndex + ? isDarkMode + ? "#ffffff" + : "#18181b" + : isDarkMode + ? "#52525b" + : "#d4d4d8", transition: "background-color 0.2s", }} /> @@ -269,6 +292,7 @@ function TourTooltip({ top: position.top, left: position.left, width: 280, + transition: "top 0.4s cubic-bezier(0.4, 0, 0.2, 1), left 0.4s cubic-bezier(0.4, 0, 0.2, 1)", }} onClick={(e) => e.stopPropagation()} onKeyDown={(e) => e.stopPropagation()} @@ -281,11 +305,19 @@ function TourTooltip({ style={{ backgroundColor: bgColor, color: textColor, - boxShadow: "0 25px 50px -12px rgba(0, 0, 0, 0.5)", + boxShadow: isDarkMode + ? "0 25px 50px -12px rgba(0, 0, 0, 0.5)" + : "0 25px 50px -12px rgba(0, 0, 0, 0.15)", }} > {/* Content */} -
+
setShouldAnimate(false)} + >

{step.title}

@@ -349,6 +381,8 @@ export function OnboardingTour() { const [isActive, setIsActive] = useState(false); const [stepIndex, setStepIndex] = useState(0); const [targetEl, setTargetEl] = useState(null); + const [spotlightTargetEl, setSpotlightTargetEl] = useState(null); + const [spotlightStepTarget, setSpotlightStepTarget] = useState(null); const [position, setPosition] = useState(null); const [targetRect, setTargetRect] = useState(null); const [mounted, setMounted] = useState(false); @@ -395,6 +429,8 @@ export function OnboardingTour() { if (el) { setIsActive(true); setTargetEl(el); + setSpotlightTargetEl(el); + setSpotlightStepTarget(TOUR_STEPS[0].target); setTargetRect(el.getBoundingClientRect()); setPosition(calculatePosition(el, TOUR_STEPS[0].placement)); } @@ -449,6 +485,17 @@ export function OnboardingTour() { } }, [isActive, updateTarget, currentStep]); + // Delay spotlight update to sync with tooltip animation + useEffect(() => { + if (targetEl && currentStep) { + const timer = setTimeout(() => { + setSpotlightTargetEl(targetEl); + setSpotlightStepTarget(currentStep.target); + }, 100); + return () => clearTimeout(timer); + } + }, [targetEl, currentStep]); + // Ensure target element is above overlay layers so content is fully visible useEffect(() => { if (!targetEl || !isActive) return; @@ -514,32 +561,60 @@ export function OnboardingTour() { } return createPortal( -
- {/* Clickable backdrop to close */} -
, + <> + +
+ {/* Clickable backdrop to close */} +
+ , document.body ); } From d75df7e5b209cb446a16587673c566fcb5fd093a Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Tue, 6 Jan 2026 19:26:40 +0200 Subject: [PATCH 39/75] BE-2: Remove duplicate-check logic and enable user-friendly auto-naming for Airtable and Confluence connector OAuth flows --- .../routes/airtable_add_connector_route.py | 55 ++++++++----------- .../routes/confluence_add_connector_route.py | 55 ++++++++----------- 2 files changed, 44 insertions(+), 66 deletions(-) diff --git a/surfsense_backend/app/routes/airtable_add_connector_route.py b/surfsense_backend/app/routes/airtable_add_connector_route.py index 9284d89e8..7e3358c6f 100644 --- a/surfsense_backend/app/routes/airtable_add_connector_route.py +++ b/surfsense_backend/app/routes/airtable_add_connector_route.py @@ -23,6 +23,7 @@ from app.db import ( from app.schemas.airtable_auth_credentials import AirtableAuthCredentialsBase from app.users import current_active_user from app.utils.oauth_security import OAuthStateManager, TokenEncryption +from app.utils.connector_naming import generate_unique_connector_name, extract_identifier_from_credentials logger = logging.getLogger(__name__) @@ -297,39 +298,27 @@ async def airtable_callback( credentials_dict = credentials.to_dict() credentials_dict["_token_encrypted"] = True - # Check if connector already exists for this search space and user - existing_connector_result = await session.execute( - select(SearchSourceConnector).filter( - SearchSourceConnector.search_space_id == space_id, - SearchSourceConnector.user_id == user_id, - SearchSourceConnector.connector_type - == SearchSourceConnectorType.AIRTABLE_CONNECTOR, - ) + # Extract unique identifier from connector credentials + connector_identifier = extract_identifier_from_credentials( + SearchSourceConnectorType.AIRTABLE_CONNECTOR, credentials_dict + ) + # Generate a unique, user-friendly connector name from credentials/account info + connector_name = generate_unique_connector_name( + SearchSourceConnectorType.AIRTABLE_CONNECTOR, connector_identifier + ) + # Create new connector + new_connector = SearchSourceConnector( + name=connector_name, + connector_type=SearchSourceConnectorType.AIRTABLE_CONNECTOR, + is_indexable=True, + config=credentials_dict, + search_space_id=space_id, + user_id=user_id, + ) + session.add(new_connector) + logger.info( + f"Created new Airtable connector for user {user_id} in space {space_id}" ) - existing_connector = existing_connector_result.scalars().first() - - if existing_connector: - # Update existing connector - existing_connector.config = credentials_dict - existing_connector.name = "Airtable Connector" - existing_connector.is_indexable = True - logger.info( - f"Updated existing Airtable connector for user {user_id} in space {space_id}" - ) - else: - # Create new connector - new_connector = SearchSourceConnector( - name="Airtable Connector", - connector_type=SearchSourceConnectorType.AIRTABLE_CONNECTOR, - is_indexable=True, - config=credentials_dict, - search_space_id=space_id, - user_id=user_id, - ) - session.add(new_connector) - logger.info( - f"Created new Airtable connector for user {user_id} in space {space_id}" - ) try: await session.commit() @@ -350,7 +339,7 @@ async def airtable_callback( await session.rollback() raise HTTPException( status_code=409, - detail=f"Integrity error: A connector with this type already exists. {e!s}", + detail=f"Database integrity error: {e!s}", ) from e except Exception as e: logger.error(f"Failed to create search source connector: {e!s}") diff --git a/surfsense_backend/app/routes/confluence_add_connector_route.py b/surfsense_backend/app/routes/confluence_add_connector_route.py index e86d411b6..a583c905a 100644 --- a/surfsense_backend/app/routes/confluence_add_connector_route.py +++ b/surfsense_backend/app/routes/confluence_add_connector_route.py @@ -26,6 +26,7 @@ from app.db import ( from app.schemas.atlassian_auth_credentials import AtlassianAuthCredentialsBase from app.users import current_active_user from app.utils.oauth_security import OAuthStateManager, TokenEncryption +from app.utils.connector_naming import generate_unique_connector_name, extract_identifier_from_credentials logger = logging.getLogger(__name__) @@ -288,39 +289,27 @@ async def confluence_callback( "_token_encrypted": True, } - # Check if connector already exists for this search space and user - existing_connector_result = await session.execute( - select(SearchSourceConnector).filter( - SearchSourceConnector.search_space_id == space_id, - SearchSourceConnector.user_id == user_id, - SearchSourceConnector.connector_type - == SearchSourceConnectorType.CONFLUENCE_CONNECTOR, - ) + # Extract unique identifier from connector credentials + connector_identifier = extract_identifier_from_credentials( + SearchSourceConnectorType.CONFLUENCE_CONNECTOR, connector_config + ) + # Generate a unique, user-friendly connector name from credentials/account info + connector_name = generate_unique_connector_name( + SearchSourceConnectorType.CONFLUENCE_CONNECTOR, connector_identifier + ) + # Create new connector + new_connector = SearchSourceConnector( + name=connector_name, + connector_type=SearchSourceConnectorType.CONFLUENCE_CONNECTOR, + is_indexable=True, + config=connector_config, + search_space_id=space_id, + user_id=user_id, + ) + session.add(new_connector) + logger.info( + f"Created new Confluence connector for user {user_id} in space {space_id}" ) - existing_connector = existing_connector_result.scalars().first() - - if existing_connector: - # Update existing connector - existing_connector.config = connector_config - existing_connector.name = "Confluence Connector" - existing_connector.is_indexable = True - logger.info( - f"Updated existing Confluence connector for user {user_id} in space {space_id}" - ) - else: - # Create new connector - new_connector = SearchSourceConnector( - name="Confluence Connector", - connector_type=SearchSourceConnectorType.CONFLUENCE_CONNECTOR, - is_indexable=True, - config=connector_config, - search_space_id=space_id, - user_id=user_id, - ) - session.add(new_connector) - logger.info( - f"Created new Confluence connector for user {user_id} in space {space_id}" - ) try: await session.commit() @@ -340,7 +329,7 @@ async def confluence_callback( await session.rollback() raise HTTPException( status_code=409, - detail=f"Integrity error: A connector with this type already exists. {e!s}", + detail=f"Database integrity error: {e!s}", ) from e except Exception as e: logger.error(f"Failed to create search source connector: {e!s}") From 933917d8fd7c963735a797993748ec167ce39a67 Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Tue, 6 Jan 2026 22:59:40 +0530 Subject: [PATCH 40/75] feat: enhance onboarding tour logic and data handling - Integrated user data fetching and validation to determine if the onboarding tour should be displayed. - Improved checks for user status by evaluating threads, documents, and connectors to identify new users. - Added localStorage management to track whether users have completed or skipped the tour. - Refactored the tour initiation logic to ensure it only runs when all necessary data is loaded and available. --- surfsense_web/components/onboarding-tour.tsx | 142 ++++++++++++++++--- 1 file changed, 125 insertions(+), 17 deletions(-) diff --git a/surfsense_web/components/onboarding-tour.tsx b/surfsense_web/components/onboarding-tour.tsx index 19d616237..1f01d8ad4 100644 --- a/surfsense_web/components/onboarding-tour.tsx +++ b/surfsense_web/components/onboarding-tour.tsx @@ -1,8 +1,16 @@ "use client"; +import { useAtomValue } from "jotai"; +import { useQuery } from "@tanstack/react-query"; +import { usePathname } from "next/navigation"; import { useTheme } from "next-themes"; import { useCallback, useEffect, useRef, useState } from "react"; import { createPortal } from "react-dom"; +import { currentUserAtom } from "@/atoms/user/user-query.atoms"; +import { activeSearchSpaceIdAtom } from "@/atoms/search-spaces/search-space-query.atoms"; +import { documentTypeCountsAtom } from "@/atoms/documents/document-query.atoms"; +import { connectorsAtom } from "@/atoms/connectors/connector-query.atoms"; +import { fetchThreads } from "@/lib/chat/thread-persistence"; interface TourStep { target: string; @@ -387,9 +395,27 @@ export function OnboardingTour() { const [targetRect, setTargetRect] = useState(null); const [mounted, setMounted] = useState(false); const { resolvedTheme } = useTheme(); + const pathname = usePathname(); const retryCountRef = useRef(0); const maxRetries = 10; + // Get user data + const { data: user } = useAtomValue(currentUserAtom); + const searchSpaceId = useAtomValue(activeSearchSpaceIdAtom); + + // Fetch threads data + const { data: threadsData } = useQuery({ + queryKey: ["threads", searchSpaceId], + queryFn: () => fetchThreads(Number(searchSpaceId), 1), // Only need to check if any exist + enabled: !!searchSpaceId, + }); + + // Get document type counts + const { data: documentTypeCounts } = useAtomValue(documentTypeCountsAtom); + + // Get connectors + const { data: connectors = [] } = useAtomValue(connectorsAtom); + const isDarkMode = resolvedTheme === "dark"; const currentStep = TOUR_STEPS[stepIndex]; @@ -422,22 +448,84 @@ export function OnboardingTour() { } }, [currentStep]); - // Start tour and find first target + // Check if tour should run: localStorage + data validation useEffect(() => { - const timer = setTimeout(() => { - const el = document.querySelector(TOUR_STEPS[0].target); - if (el) { - setIsActive(true); - setTargetEl(el); - setSpotlightTargetEl(el); - setSpotlightStepTarget(TOUR_STEPS[0].target); - setTargetRect(el.getBoundingClientRect()); - setPosition(calculatePosition(el, TOUR_STEPS[0].placement)); - } - }, 1000); + // Don't check if not mounted or no user + if (!mounted || !user?.id || !searchSpaceId) return; + // Check if on new-chat page + const isNewChatPage = pathname?.includes("/new-chat"); + if (!isNewChatPage) return; + + // Wait for all data to be loaded before making decision + // Data is considered loaded when: + // - threadsData is defined (query completed, even if empty) + // - documentTypeCounts is defined (query completed, even if empty object) + // - connectors is an array (always defined with default []) + // If searchSpaceId is not set, connectors query won't run, but that's okay + const dataLoaded = threadsData !== undefined && documentTypeCounts !== undefined; + if (!dataLoaded) return; + + // Check localStorage first (fast check) + const tourKey = `surfsense-tour-${user.id}`; + const hasSeenTour = localStorage.getItem(tourKey); + if (hasSeenTour === "true") { + return; // User has seen tour, don't show + } + + // Validate user is actually new (reliable check) + const threads = threadsData?.threads ?? []; + const hasThreads = threads.length > 0; + + // Check document counts - sum all document type counts + const totalDocuments = documentTypeCounts + ? Object.values(documentTypeCounts).reduce((sum, count) => sum + count, 0) + : 0; + const hasDocuments = totalDocuments > 0; + + const hasConnectors = connectors.length > 0; + + // User is new if they have no threads, documents, or connectors + const isNewUser = !hasThreads && !hasDocuments && !hasConnectors; + + // If user has data but localStorage was cleared, mark as seen + if (!isNewUser) { + localStorage.setItem(tourKey, "true"); + return; + } + + // User is new and hasn't seen tour - wait for DOM elements and start tour + const checkAndStartTour = () => { + // Check if both required elements exist + const connectorEl = document.querySelector(TOUR_STEPS[0].target); + const documentsEl = document.querySelector(TOUR_STEPS[1].target); + + if (connectorEl && documentsEl) { + // Both elements found, start tour + setIsActive(true); + setTargetEl(connectorEl); + setSpotlightTargetEl(connectorEl); + setSpotlightStepTarget(TOUR_STEPS[0].target); + setTargetRect(connectorEl.getBoundingClientRect()); + setPosition(calculatePosition(connectorEl, TOUR_STEPS[0].placement)); + } else { + // Retry after delay + setTimeout(checkAndStartTour, 200); + } + }; + + // Start checking after initial delay + const timer = setTimeout(checkAndStartTour, 500); return () => clearTimeout(timer); - }, []); + }, [ + mounted, + user?.id, + searchSpaceId, + pathname, + threadsData, + documentTypeCounts, + connectors, + ]); // Update position on resize/scroll useEffect(() => { @@ -524,9 +612,14 @@ export function OnboardingTour() { retryCountRef.current = 0; setStepIndex(stepIndex + 1); } else { + // Tour completed - save to localStorage + if (user?.id) { + const tourKey = `surfsense-tour-${user.id}`; + localStorage.setItem(tourKey, "true"); + } setIsActive(false); } - }, [stepIndex]); + }, [stepIndex, user?.id]); const handlePrev = useCallback(() => { if (stepIndex > 0) { @@ -536,24 +629,39 @@ export function OnboardingTour() { }, [stepIndex]); const handleSkip = useCallback(() => { + // Tour skipped - save to localStorage + if (user?.id) { + const tourKey = `surfsense-tour-${user.id}`; + localStorage.setItem(tourKey, "true"); + } setIsActive(false); - }, []); + }, [user?.id]); // Handle overlay click to close const handleOverlayClick = useCallback(() => { + // Tour closed - save to localStorage + if (user?.id) { + const tourKey = `surfsense-tour-${user.id}`; + localStorage.setItem(tourKey, "true"); + } setIsActive(false); - }, []); + }, [user?.id]); // Handle escape key useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === "Escape" && isActive) { + // Tour closed via escape - save to localStorage + if (user?.id) { + const tourKey = `surfsense-tour-${user.id}`; + localStorage.setItem(tourKey, "true"); + } setIsActive(false); } }; window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); - }, [isActive]); + }, [isActive, user?.id]); // Don't render if not active or not mounted if (!mounted || !isActive) { From e08eb7920cfbc0fed3c4f1233d100255352f9264 Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Tue, 6 Jan 2026 23:01:28 +0530 Subject: [PATCH 41/75] fix: update the content for document --- surfsense_web/components/onboarding-tour.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/surfsense_web/components/onboarding-tour.tsx b/surfsense_web/components/onboarding-tour.tsx index 1f01d8ad4..8711652f7 100644 --- a/surfsense_web/components/onboarding-tour.tsx +++ b/surfsense_web/components/onboarding-tour.tsx @@ -29,7 +29,7 @@ const TOUR_STEPS: TourStep[] = [ { target: '[data-joyride="documents-sidebar"]', title: "Manage your documents", - content: "Access and manage all your uploaded documents from the sidebar.", + content: "Access and manage all your uploaded documents.", placement: "right", }, ]; From 6939eb975d8e7ea5a2ab06d40c686933ad4a5b3c Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Tue, 6 Jan 2026 23:03:51 +0530 Subject: [PATCH 42/75] chore: ran frontend lint --- surfsense_web/components/onboarding-tour.tsx | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/surfsense_web/components/onboarding-tour.tsx b/surfsense_web/components/onboarding-tour.tsx index 8711652f7..9f7a95bba 100644 --- a/surfsense_web/components/onboarding-tour.tsx +++ b/surfsense_web/components/onboarding-tour.tsx @@ -517,15 +517,7 @@ export function OnboardingTour() { // Start checking after initial delay const timer = setTimeout(checkAndStartTour, 500); return () => clearTimeout(timer); - }, [ - mounted, - user?.id, - searchSpaceId, - pathname, - threadsData, - documentTypeCounts, - connectors, - ]); + }, [mounted, user?.id, searchSpaceId, pathname, threadsData, documentTypeCounts, connectors]); // Update position on resize/scroll useEffect(() => { From d979c156f8043a9a48f5686067a7f287bffd52ad Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Tue, 6 Jan 2026 19:38:11 +0200 Subject: [PATCH 43/75] BE-2: Enforce unique connector names per user and search space (idempotent migration) --- ...58_unique_connector_name_per_space_user.py | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 surfsense_backend/alembic/versions/58_unique_connector_name_per_space_user.py diff --git a/surfsense_backend/alembic/versions/58_unique_connector_name_per_space_user.py b/surfsense_backend/alembic/versions/58_unique_connector_name_per_space_user.py new file mode 100644 index 000000000..b840af267 --- /dev/null +++ b/surfsense_backend/alembic/versions/58_unique_connector_name_per_space_user.py @@ -0,0 +1,53 @@ +""" +Add unique constraint for (search_space_id, user_id, name) on search_source_connectors. + +Revision ID: 58 +Revises: 57 +Create Date: 2026-01-06 14:00:00.000000 + +""" + +from collections.abc import Sequence +from alembic import op + +revision: str = "58" +down_revision: str | None = "57" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None + +from sqlalchemy import text + +def upgrade() -> None: + connection = op.get_bind() + constraint_exists = connection.execute( + text(""" + SELECT 1 FROM information_schema.table_constraints + WHERE table_name='search_source_connectors' + AND constraint_type='UNIQUE' + AND constraint_name='uq_searchspace_user_connector_name' + """) + ).scalar() + if not constraint_exists: + op.create_unique_constraint( + "uq_searchspace_user_connector_name", + "search_source_connectors", + ["search_space_id", "user_id", "name"] + ) + +def downgrade() -> None: + connection = op.get_bind() + constraint_exists = connection.execute( + text(""" + SELECT 1 FROM information_schema.table_constraints + WHERE table_name='search_source_connectors' + AND constraint_type='UNIQUE' + AND constraint_name='uq_searchspace_user_connector_name' + """) + ).scalar() + if constraint_exists: + op.drop_constraint( + "uq_searchspace_user_connector_name", + "search_source_connectors", + type_="unique" + ) + From 9f48f22d2893d83d633fa724f8b04c5de114abc2 Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Wed, 7 Jan 2026 01:43:11 +0530 Subject: [PATCH 44/75] feat: enhance onboarding tour user ID tracking - Added logic to track previous user ID to detect changes and reset tour state accordingly. - Updated localStorage checks to ensure the onboarding tour is displayed only for new users who haven't seen it. - Improved validation logic to prevent auto-marking the tour as seen if the user has existing data. --- surfsense_web/components/onboarding-tour.tsx | 36 ++++++++++++++++---- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/surfsense_web/components/onboarding-tour.tsx b/surfsense_web/components/onboarding-tour.tsx index 9f7a95bba..0fc43160a 100644 --- a/surfsense_web/components/onboarding-tour.tsx +++ b/surfsense_web/components/onboarding-tour.tsx @@ -398,6 +398,8 @@ export function OnboardingTour() { const pathname = usePathname(); const retryCountRef = useRef(0); const maxRetries = 10; + // Track previous user ID to detect user changes + const previousUserIdRef = useRef(null); // Get user data const { data: user } = useAtomValue(currentUserAtom); @@ -448,7 +450,7 @@ export function OnboardingTour() { } }, [currentStep]); - // Check if tour should run: localStorage + data validation + // Check if tour should run: localStorage + data validation with user ID tracking useEffect(() => { // Don't check if not mounted or no user if (!mounted || !user?.id || !searchSpaceId) return; @@ -466,11 +468,31 @@ export function OnboardingTour() { const dataLoaded = threadsData !== undefined && documentTypeCounts !== undefined; if (!dataLoaded) return; - // Check localStorage first (fast check) - const tourKey = `surfsense-tour-${user.id}`; + const currentUserId = user.id; + const previousUserId = previousUserIdRef.current; + + // Detect user change - if user ID changed, reset tour state + if (previousUserId !== null && previousUserId !== currentUserId) { + // User changed - reset tour state and re-evaluate for new user + setIsActive(false); + setStepIndex(0); + setTargetEl(null); + setSpotlightTargetEl(null); + setSpotlightStepTarget(null); + setPosition(null); + setTargetRect(null); + retryCountRef.current = 0; + } + + // Update previous user ID ref + previousUserIdRef.current = currentUserId; + + // Check localStorage for CURRENT user ID (not stale cache) + // This ensures we check the correct user's tour status + const tourKey = `surfsense-tour-${currentUserId}`; const hasSeenTour = localStorage.getItem(tourKey); if (hasSeenTour === "true") { - return; // User has seen tour, don't show + return; // Current user has seen tour, don't show } // Validate user is actually new (reliable check) @@ -488,10 +510,10 @@ export function OnboardingTour() { // User is new if they have no threads, documents, or connectors const isNewUser = !hasThreads && !hasDocuments && !hasConnectors; - // If user has data but localStorage was cleared, mark as seen + // Only show tour if user is new and hasn't seen it + // Don't auto-mark as seen if user has data - let them explicitly dismiss it if (!isNewUser) { - localStorage.setItem(tourKey, "true"); - return; + return; // User has data, don't show tour } // User is new and hasn't seen tour - wait for DOM elements and start tour From f2724ea162fd230a0f66dc612f5c1b70b463312a Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Wed, 7 Jan 2026 03:00:56 +0530 Subject: [PATCH 45/75] feat: enhance Airtable integration with OAuth support and date validation - Introduced AirtableHistoryConnector to manage OAuth-based authentication and token refresh for Airtable API access. - Added date string validation in AirtableConnector to ensure valid date inputs before processing. - Updated indexing logic to utilize the new AirtableHistoryConnector, improving credential management and token handling. --- .../app/connectors/airtable_connector.py | 6 + .../app/connectors/airtable_history.py | 175 ++++++++++++++++++ .../app/connectors/slack_history.py | 2 +- .../routes/airtable_add_connector_route.py | 18 +- .../connector_indexers/airtable_indexer.py | 156 +++++----------- 5 files changed, 247 insertions(+), 110 deletions(-) create mode 100644 surfsense_backend/app/connectors/airtable_history.py diff --git a/surfsense_backend/app/connectors/airtable_connector.py b/surfsense_backend/app/connectors/airtable_connector.py index 840b2276c..7b9209bec 100644 --- a/surfsense_backend/app/connectors/airtable_connector.py +++ b/surfsense_backend/app/connectors/airtable_connector.py @@ -294,6 +294,12 @@ class AirtableConnector: Tuple of (records, error_message) """ try: + # Validate date strings before parsing + if not start_date or start_date.lower() in ("undefined", "null", "none"): + return [], "Invalid start_date: date string is required" + if not end_date or end_date.lower() in ("undefined", "null", "none"): + return [], "Invalid end_date: date string is required" + # Parse and validate dates start_dt = isoparse(start_date) end_dt = isoparse(end_date) diff --git a/surfsense_backend/app/connectors/airtable_history.py b/surfsense_backend/app/connectors/airtable_history.py new file mode 100644 index 000000000..64f6465fe --- /dev/null +++ b/surfsense_backend/app/connectors/airtable_history.py @@ -0,0 +1,175 @@ +""" +Airtable OAuth Connector. + +Handles OAuth-based authentication and token refresh for Airtable API access. +""" + +import logging + +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.future import select + +from app.config import config +from app.connectors.airtable_connector import AirtableConnector +from app.db import SearchSourceConnector +from app.routes.airtable_add_connector_route import refresh_airtable_token +from app.schemas.airtable_auth_credentials import AirtableAuthCredentialsBase +from app.utils.oauth_security import TokenEncryption + +logger = logging.getLogger(__name__) + + +class AirtableHistoryConnector: + """ + Airtable connector with OAuth support and automatic token refresh. + + This connector uses OAuth 2.0 access tokens to authenticate with the + Airtable API. It automatically refreshes expired tokens when needed. + """ + + def __init__( + self, + session: AsyncSession, + connector_id: int, + credentials: AirtableAuthCredentialsBase | None = None, + ): + """ + Initialize the AirtableHistoryConnector with auto-refresh capability. + + Args: + session: Database session for updating connector + connector_id: Connector ID for direct updates + credentials: Airtable OAuth credentials (optional, will be loaded from DB if not provided) + """ + self._session = session + self._connector_id = connector_id + self._credentials = credentials + self._airtable_connector: AirtableConnector | None = None + + async def _get_valid_token(self) -> str: + """ + Get valid Airtable access token, refreshing if needed. + + Returns: + Valid access token + + Raises: + ValueError: If credentials are missing or invalid + Exception: If token refresh fails + """ + # Load credentials from DB if not provided + if self._credentials is None: + result = await self._session.execute( + select(SearchSourceConnector).filter( + SearchSourceConnector.id == self._connector_id + ) + ) + connector = result.scalars().first() + + if not connector: + raise ValueError(f"Connector {self._connector_id} not found") + + config_data = connector.config.copy() + + # Decrypt credentials if they are encrypted + token_encrypted = config_data.get("_token_encrypted", False) + if token_encrypted and config.SECRET_KEY: + try: + token_encryption = TokenEncryption(config.SECRET_KEY) + + # Decrypt sensitive fields + if config_data.get("access_token"): + config_data["access_token"] = token_encryption.decrypt_token( + config_data["access_token"] + ) + if config_data.get("refresh_token"): + config_data["refresh_token"] = token_encryption.decrypt_token( + config_data["refresh_token"] + ) + + logger.info( + f"Decrypted Airtable credentials for connector {self._connector_id}" + ) + except Exception as e: + logger.error( + f"Failed to decrypt Airtable credentials for connector {self._connector_id}: {e!s}" + ) + raise ValueError( + f"Failed to decrypt Airtable credentials: {e!s}" + ) from e + + try: + self._credentials = AirtableAuthCredentialsBase.from_dict(config_data) + except Exception as e: + raise ValueError(f"Invalid Airtable credentials: {e!s}") from e + + # Check if token is expired and refreshable + if self._credentials.is_expired and self._credentials.is_refreshable: + try: + logger.info( + f"Airtable token expired for connector {self._connector_id}, refreshing..." + ) + + # Get connector for refresh + result = await self._session.execute( + select(SearchSourceConnector).filter( + SearchSourceConnector.id == self._connector_id + ) + ) + connector = result.scalars().first() + + if not connector: + raise RuntimeError( + f"Connector {self._connector_id} not found; cannot refresh token." + ) + + # Refresh token + connector = await refresh_airtable_token(self._session, connector) + + # Reload credentials after refresh + config_data = connector.config.copy() + token_encrypted = config_data.get("_token_encrypted", False) + if token_encrypted and config.SECRET_KEY: + token_encryption = TokenEncryption(config.SECRET_KEY) + if config_data.get("access_token"): + config_data["access_token"] = token_encryption.decrypt_token( + config_data["access_token"] + ) + if config_data.get("refresh_token"): + config_data["refresh_token"] = token_encryption.decrypt_token( + config_data["refresh_token"] + ) + + self._credentials = AirtableAuthCredentialsBase.from_dict(config_data) + + # Invalidate cached connector so it's recreated with new token + self._airtable_connector = None + + logger.info( + f"Successfully refreshed Airtable token for connector {self._connector_id}" + ) + except Exception as e: + logger.error( + f"Failed to refresh Airtable token for connector {self._connector_id}: {e!s}" + ) + raise Exception( + f"Failed to refresh Airtable OAuth credentials: {e!s}" + ) from e + + return self._credentials.access_token + + async def _get_connector(self) -> AirtableConnector: + """ + Get or create AirtableConnector with valid token. + + Returns: + AirtableConnector instance + """ + if self._airtable_connector is None: + # Ensure we have valid credentials (this will refresh if needed) + await self._get_valid_token() + # Use the credentials object which is now guaranteed to be valid + if not self._credentials: + raise ValueError("Credentials not loaded") + self._airtable_connector = AirtableConnector(self._credentials) + return self._airtable_connector diff --git a/surfsense_backend/app/connectors/slack_history.py b/surfsense_backend/app/connectors/slack_history.py index dbf43bb24..2b36b9f96 100644 --- a/surfsense_backend/app/connectors/slack_history.py +++ b/surfsense_backend/app/connectors/slack_history.py @@ -377,7 +377,7 @@ class SlackHistory: else: raise # Re-raise to outer handler for not_in_channel or other SlackApiErrors - if not current_api_call_successful: + if not current_api_call_successful or result is None: continue # Retry the current page fetch due to handled rate limit # Process result if successful diff --git a/surfsense_backend/app/routes/airtable_add_connector_route.py b/surfsense_backend/app/routes/airtable_add_connector_route.py index 9284d89e8..c45930a83 100644 --- a/surfsense_backend/app/routes/airtable_add_connector_route.py +++ b/surfsense_backend/app/routes/airtable_add_connector_route.py @@ -371,7 +371,7 @@ async def airtable_callback( async def refresh_airtable_token( session: AsyncSession, connector: SearchSourceConnector -): +) -> SearchSourceConnector: """ Refresh the Airtable access token for a connector. @@ -401,6 +401,12 @@ async def refresh_airtable_token( status_code=500, detail="Failed to decrypt stored refresh token" ) from e + if not refresh_token: + raise HTTPException( + status_code=400, + detail="No refresh token available. Please re-authenticate.", + ) + auth_header = make_basic_auth_header( config.AIRTABLE_CLIENT_ID, config.AIRTABLE_CLIENT_SECRET ) @@ -425,8 +431,14 @@ async def refresh_airtable_token( ) if token_response.status_code != 200: + error_detail = token_response.text + try: + error_json = token_response.json() + error_detail = error_json.get("error_description", error_detail) + except Exception: + pass raise HTTPException( - status_code=400, detail="Token refresh failed: {token_response.text}" + status_code=400, detail=f"Token refresh failed: {error_detail}" ) token_json = token_response.json() @@ -468,6 +480,8 @@ async def refresh_airtable_token( ) return connector + except HTTPException: + raise except Exception as e: raise HTTPException( status_code=500, detail=f"Failed to refresh Airtable token: {e!s}" diff --git a/surfsense_backend/app/tasks/connector_indexers/airtable_indexer.py b/surfsense_backend/app/tasks/connector_indexers/airtable_indexer.py index 3ea6dccc9..4d5a33b79 100644 --- a/surfsense_backend/app/tasks/connector_indexers/airtable_indexer.py +++ b/surfsense_backend/app/tasks/connector_indexers/airtable_indexer.py @@ -6,10 +6,8 @@ from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.ext.asyncio import AsyncSession from app.config import config -from app.connectors.airtable_connector import AirtableConnector +from app.connectors.airtable_history import AirtableHistoryConnector from app.db import Document, DocumentType, SearchSourceConnectorType -from app.routes.airtable_add_connector_route import refresh_airtable_token -from app.schemas.airtable_auth_credentials import AirtableAuthCredentialsBase from app.services.llm_service import get_user_long_context_llm from app.services.task_logging_service import TaskLoggingService from app.utils.document_converters import ( @@ -18,7 +16,6 @@ from app.utils.document_converters import ( generate_document_summary, generate_unique_identifier_hash, ) -from app.utils.oauth_security import TokenEncryption from .base import ( calculate_date_range, @@ -85,76 +82,11 @@ async def index_airtable_records( ) return 0, f"Connector with ID {connector_id} not found" - # Create credentials from connector config - config_data = ( - connector.config.copy() - ) # Work with a copy to avoid modifying original - - # Decrypt tokens if they are encrypted (only when explicitly marked) - token_encrypted = config_data.get("_token_encrypted", False) - if token_encrypted: - # Tokens are explicitly marked as encrypted, attempt decryption - if not config.SECRET_KEY: - await task_logger.log_task_failure( - log_entry, - f"SECRET_KEY not configured but tokens are marked as encrypted for connector {connector_id}", - "Missing SECRET_KEY for token decryption", - {"error_type": "MissingSecretKey"}, - ) - return 0, "SECRET_KEY not configured but tokens are marked as encrypted" - try: - token_encryption = TokenEncryption(config.SECRET_KEY) - - # Decrypt access_token - if config_data.get("access_token"): - config_data["access_token"] = token_encryption.decrypt_token( - config_data["access_token"] - ) - logger.info( - f"Decrypted Airtable access token for connector {connector_id}" - ) - - # Decrypt refresh_token if present - if config_data.get("refresh_token"): - config_data["refresh_token"] = token_encryption.decrypt_token( - config_data["refresh_token"] - ) - logger.info( - f"Decrypted Airtable refresh token for connector {connector_id}" - ) - except Exception as e: - await task_logger.log_task_failure( - log_entry, - f"Failed to decrypt Airtable tokens for connector {connector_id}: {e!s}", - "Token decryption failed", - {"error_type": "TokenDecryptionError"}, - ) - return 0, f"Failed to decrypt Airtable tokens: {e!s}" - # If _token_encrypted is False or not set, treat tokens as plaintext - - try: - credentials = AirtableAuthCredentialsBase.from_dict(config_data) - except Exception as e: - await task_logger.log_task_failure( - log_entry, - f"Invalid Airtable credentials in connector {connector_id}", - str(e), - {"error_type": "InvalidCredentials"}, - ) - return 0, f"Invalid Airtable credentials: {e!s}" - - # Check if credentials are expired - if credentials.is_expired: - await task_logger.log_task_failure( - log_entry, - f"Airtable credentials expired for connector {connector_id}", - "Credentials expired", - {"error_type": "ExpiredCredentials"}, - ) - - connector = await refresh_airtable_token(session, connector) - - # return 0, "Airtable credentials have expired. Please re-authenticate." + # Normalize "undefined" strings to None (from frontend) + if start_date == "undefined" or start_date == "": + start_date = None + if end_date == "undefined" or end_date == "": + end_date = None # Calculate date range for indexing start_date_str, end_date_str = calculate_date_range( @@ -166,8 +98,9 @@ async def index_airtable_records( f"from {start_date_str} to {end_date_str}" ) - # Initialize Airtable connector - airtable_connector = AirtableConnector(credentials) + # Initialize Airtable history connector with auto-refresh capability + airtable_history = AirtableHistoryConnector(session, connector_id) + airtable_connector = await airtable_history._get_connector() total_processed = 0 try: @@ -459,47 +392,56 @@ async def index_airtable_records( documents_skipped += 1 continue # Skip this message and continue with others - # Update the last_indexed_at timestamp for the connector only if requested - total_processed = documents_indexed - if total_processed > 0: - await update_connector_last_indexed( - session, connector, update_last_indexed - ) + # Accumulate total processed across all tables + total_processed += documents_indexed # Final commit for any remaining documents not yet committed in batches - logger.info( - f"Final commit: Total {documents_indexed} Airtable records processed" - ) - await session.commit() - logger.info( - "Successfully committed all Airtable document changes to database" - ) + if documents_indexed > 0: + logger.info( + f"Final commit for table {table_name}: {documents_indexed} Airtable records processed" + ) + await session.commit() + logger.info( + f"Successfully committed all Airtable document changes for table {table_name}" + ) - # Log success - await task_logger.log_task_success( - log_entry, - f"Successfully completed Airtable indexing for connector {connector_id}", - { - "events_processed": total_processed, - "documents_indexed": documents_indexed, - "documents_skipped": documents_skipped, - "skipped_messages_count": len(skipped_messages), - }, - ) + # Update the last_indexed_at timestamp for the connector only if requested + # (after all tables in all bases are processed) + if total_processed > 0: + await update_connector_last_indexed( + session, connector, update_last_indexed + ) - logger.info( - f"Airtable indexing completed: {documents_indexed} new records, {documents_skipped} skipped" - ) - return ( - total_processed, - None, - ) # Return None as the error message to indicate success + # Log success after processing all bases and tables + await task_logger.log_task_success( + log_entry, + f"Successfully completed Airtable indexing for connector {connector_id}", + { + "events_processed": total_processed, + "documents_indexed": total_processed, + }, + ) + + logger.info( + f"Airtable indexing completed: {total_processed} total records processed" + ) + return ( + total_processed, + None, + ) # Return None as the error message to indicate success except Exception as e: logger.error( f"Fetching Airtable bases for connector {connector_id} failed: {e!s}", exc_info=True, ) + await task_logger.log_task_failure( + log_entry, + f"Failed to fetch Airtable bases for connector {connector_id}", + str(e), + {"error_type": type(e).__name__}, + ) + return 0, f"Failed to fetch Airtable bases: {e!s}" except SQLAlchemyError as db_error: await session.rollback() From bd8821c48915f87c49f0106b8e9690c09fbddb01 Mon Sep 17 00:00:00 2001 From: "DESKTOP-RTLN3BA\\$punk" Date: Tue, 6 Jan 2026 17:41:50 -0800 Subject: [PATCH 46/75] feat: updated docs and fixed docker runtime vars injection - Modified Dockerfile to use placeholder values for frontend environment variables, allowing for runtime substitution. - Enhanced entrypoint script to apply runtime environment variable configuration, replacing placeholders in JavaScript files with actual values. - Updated documentation paths in MDX files for Google OAuth images and added detailed setup guides for Discord, Linear, Notion, and Slack OAuth integrations. --- Dockerfile.allinone | 19 ++-- scripts/docker/entrypoint-allinone.sh | 32 +++++- .../content/docs/connectors/discord.mdx | 78 +++++++++++---- .../content/docs/connectors/linear.mdx | 70 +++++++++---- .../content/docs/connectors/notion.mdx | 87 ++++++++++++---- .../content/docs/connectors/slack.mdx | 94 ++++++++++++++---- surfsense_web/content/docs/index.mdx | 8 +- surfsense_web/mdx-components.tsx | 9 +- .../discord/discord-bot-permissions.png | Bin 0 -> 107493 bytes .../discord/discord-bot-settings.png | Bin 0 -> 166608 bytes .../discord/discord-general-info.png | Bin 0 -> 164831 bytes .../connectors/discord/discord-oauth2.png | Bin 0 -> 74013 bytes .../google}/google_oauth_client.png | Bin .../google}/google_oauth_config.png | Bin .../google}/google_oauth_people_api.png | Bin .../google}/google_oauth_screen.png | Bin .../connectors/linear/linear-api-settings.png | Bin 0 -> 119439 bytes .../linear/linear-new-application.png | Bin 0 -> 164593 bytes .../linear/linear-oauth-credentials.png | Bin 0 -> 22821 bytes .../notion/notion-integration-config.png | Bin 0 -> 84035 bytes .../notion/notion-integrations-page.png | Bin 0 -> 72879 bytes .../notion/notion-new-integration-form.png | Bin 0 -> 69727 bytes .../slack/slack-app-credentials.png | Bin 0 -> 171130 bytes .../connectors/slack/slack-create-app.png | Bin 0 -> 40543 bytes .../connectors/slack/slack-distribution.png | Bin 0 -> 174292 bytes .../connectors/slack/slack-name-workspace.png | Bin 0 -> 58440 bytes .../connectors/slack/slack-redirect-urls.png | Bin 0 -> 36298 bytes .../docs/connectors/slack/slack-scopes.png | Bin 0 -> 86436 bytes 28 files changed, 308 insertions(+), 89 deletions(-) create mode 100644 surfsense_web/public/docs/connectors/discord/discord-bot-permissions.png create mode 100644 surfsense_web/public/docs/connectors/discord/discord-bot-settings.png create mode 100644 surfsense_web/public/docs/connectors/discord/discord-general-info.png create mode 100644 surfsense_web/public/docs/connectors/discord/discord-oauth2.png rename surfsense_web/public/docs/{ => connectors/google}/google_oauth_client.png (100%) rename surfsense_web/public/docs/{ => connectors/google}/google_oauth_config.png (100%) rename surfsense_web/public/docs/{ => connectors/google}/google_oauth_people_api.png (100%) rename surfsense_web/public/docs/{ => connectors/google}/google_oauth_screen.png (100%) create mode 100644 surfsense_web/public/docs/connectors/linear/linear-api-settings.png create mode 100644 surfsense_web/public/docs/connectors/linear/linear-new-application.png create mode 100644 surfsense_web/public/docs/connectors/linear/linear-oauth-credentials.png create mode 100644 surfsense_web/public/docs/connectors/notion/notion-integration-config.png create mode 100644 surfsense_web/public/docs/connectors/notion/notion-integrations-page.png create mode 100644 surfsense_web/public/docs/connectors/notion/notion-new-integration-form.png create mode 100644 surfsense_web/public/docs/connectors/slack/slack-app-credentials.png create mode 100644 surfsense_web/public/docs/connectors/slack/slack-create-app.png create mode 100644 surfsense_web/public/docs/connectors/slack/slack-distribution.png create mode 100644 surfsense_web/public/docs/connectors/slack/slack-name-workspace.png create mode 100644 surfsense_web/public/docs/connectors/slack/slack-redirect-urls.png create mode 100644 surfsense_web/public/docs/connectors/slack/slack-scopes.png diff --git a/Dockerfile.allinone b/Dockerfile.allinone index 12eee5c90..1c04ffb99 100644 --- a/Dockerfile.allinone +++ b/Dockerfile.allinone @@ -37,14 +37,11 @@ COPY surfsense_web/ ./ # Run fumadocs-mdx postinstall now that source files are available RUN pnpm fumadocs-mdx -# Build args for frontend -ARG NEXT_PUBLIC_FASTAPI_BACKEND_URL=http://localhost:8000 -ARG NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE=LOCAL -ARG NEXT_PUBLIC_ETL_SERVICE=DOCLING - -ENV NEXT_PUBLIC_FASTAPI_BACKEND_URL=$NEXT_PUBLIC_FASTAPI_BACKEND_URL -ENV NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE=$NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE -ENV NEXT_PUBLIC_ETL_SERVICE=$NEXT_PUBLIC_ETL_SERVICE +# Build with placeholder values that will be replaced at runtime +# These unique strings allow runtime substitution via entrypoint script +ENV NEXT_PUBLIC_FASTAPI_BACKEND_URL=__NEXT_PUBLIC_FASTAPI_BACKEND_URL__ +ENV NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE=__NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE__ +ENV NEXT_PUBLIC_ETL_SERVICE=__NEXT_PUBLIC_ETL_SERVICE__ # Build RUN pnpm run build @@ -233,6 +230,12 @@ ENV AUTH_TYPE=LOCAL ENV ETL_SERVICE=DOCLING ENV EMBEDDING_MODEL=sentence-transformers/all-MiniLM-L6-v2 +# Frontend configuration (can be overridden at runtime) +# These are injected into the Next.js build at container startup +ENV NEXT_PUBLIC_FASTAPI_BACKEND_URL=http://localhost:8000 +ENV NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE=LOCAL +ENV NEXT_PUBLIC_ETL_SERVICE=DOCLING + # Data volume VOLUME ["/data"] diff --git a/scripts/docker/entrypoint-allinone.sh b/scripts/docker/entrypoint-allinone.sh index 427256f6d..8248968ab 100644 --- a/scripts/docker/entrypoint-allinone.sh +++ b/scripts/docker/entrypoint-allinone.sh @@ -96,6 +96,30 @@ if [ -d /app/frontend/.next/standalone ]; then cp -r /app/frontend/.next/static /app/frontend/.next/static 2>/dev/null || true fi +# ================================================ +# Runtime Environment Variable Replacement +# ================================================ +# Next.js NEXT_PUBLIC_* vars are baked in at build time. +# This replaces placeholder values with actual runtime env vars. +echo "🔧 Applying runtime environment configuration..." + +# Set defaults if not provided +NEXT_PUBLIC_FASTAPI_BACKEND_URL="${NEXT_PUBLIC_FASTAPI_BACKEND_URL:-http://localhost:8000}" +NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE="${NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE:-LOCAL}" +NEXT_PUBLIC_ETL_SERVICE="${NEXT_PUBLIC_ETL_SERVICE:-DOCLING}" + +# Replace placeholders in all JS files +find /app/frontend -type f \( -name "*.js" -o -name "*.json" \) -exec sed -i \ + -e "s|__NEXT_PUBLIC_FASTAPI_BACKEND_URL__|${NEXT_PUBLIC_FASTAPI_BACKEND_URL}|g" \ + -e "s|__NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE__|${NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE}|g" \ + -e "s|__NEXT_PUBLIC_ETL_SERVICE__|${NEXT_PUBLIC_ETL_SERVICE}|g" \ + {} + + +echo "✅ Environment configuration applied" +echo " Backend URL: ${NEXT_PUBLIC_FASTAPI_BACKEND_URL}" +echo " Auth Type: ${NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE}" +echo " ETL Service: ${NEXT_PUBLIC_ETL_SERVICE}" + # ================================================ # Run database migrations # ================================================ @@ -135,10 +159,10 @@ echo "===========================================" echo " 📋 Configuration" echo "===========================================" echo " Frontend URL: http://localhost:3000" -echo " Backend API: http://localhost:8000" -echo " API Docs: http://localhost:8000/docs" -echo " Auth Type: ${AUTH_TYPE:-LOCAL}" -echo " ETL Service: ${ETL_SERVICE:-DOCLING}" +echo " Backend API: ${NEXT_PUBLIC_FASTAPI_BACKEND_URL}" +echo " API Docs: ${NEXT_PUBLIC_FASTAPI_BACKEND_URL}/docs" +echo " Auth Type: ${NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE}" +echo " ETL Service: ${NEXT_PUBLIC_ETL_SERVICE}" echo " TTS Service: ${TTS_SERVICE}" echo " STT Service: ${STT_SERVICE}" echo "===========================================" diff --git a/surfsense_web/content/docs/connectors/discord.mdx b/surfsense_web/content/docs/connectors/discord.mdx index 2dd2c1205..6bb64e7e7 100644 --- a/surfsense_web/content/docs/connectors/discord.mdx +++ b/surfsense_web/content/docs/connectors/discord.mdx @@ -3,30 +3,74 @@ title: Discord description: Connect your Discord servers to SurfSense --- -# Discord Connector +# Discord OAuth Integration Setup Guide -Index your Discord server conversations and content. +This guide walks you through setting up a Discord OAuth integration for SurfSense. -## Prerequisites +## Step 1: Create a New Discord Application -- A Discord account -- Server admin permissions +1. Navigate to [discord.com/developers/applications](https://discord.com/developers/applications) +2. Click **"New Application"** +3. Enter the application name: `SurfSense` +4. Click **"Create"** -## Setup +## Step 2: Configure General Information -1. Navigate to your Search Space settings -2. Click on **Add Connector** -3. Select **Discord** from the list -4. Authorize SurfSense to access your Discord server -5. Select the channels you want to index +On the **General Information** page, fill in the details: -## What Gets Indexed +| Field | Value | +|-------|-------| +| **App Icon** | Upload an icon (1024x1024px, PNG/GIF/JPG/WEBP, max 10MB) | +| **Name** | `SurfSense` | +| **Description** | Connect any LLM to your internal knowledge sources and chat with it in real time alongside your team. | +| **Tags** | Add up to 5 tags (optional) | -- Text channel messages -- Thread messages -- Shared files and links +You'll also see your **Application ID** and **Public Key** on this page. -## Sync Frequency +![General Information](/docs/connectors/discord/discord-general-info.png) -The Discord connector supports scheduled syncing to keep your content up to date. +## Step 3: Configure OAuth2 Settings +1. In the left sidebar, click **"OAuth2"** +2. Copy your **Client ID** and **Client Secret** (click to reveal) +3. Under **Redirects**, click **"Add Another"** and enter: + ``` + http://localhost:8000/api/v1/auth/discord/connector/callback + ``` + +> ⚠️ Keep **Public Client** disabled (off) since SurfSense uses a server to make requests. + +![OAuth2 Configuration](/docs/connectors/discord/discord-oauth2.png) + +## Step 4: Configure Bot Settings + +1. In the left sidebar, click **"Bot"** +2. Configure the **Authorization Flow**: + - ✅ **Public Bot** - Enable to allow anyone to add the bot to servers + +3. Enable **Privileged Gateway Intents**: + - ✅ **Server Members Intent** - Required to receive GUILD_MEMBERS events + - ✅ **Message Content Intent** - Required to receive message content + +> ⚠️ Once your bot reaches 100+ servers, these intents will require verification and approval. + +![Bot Settings](/docs/connectors/discord/discord-bot-settings.png) + +--- + +## Running SurfSense with Discord Connector + +Add the Discord environment variables to your Docker run command: + +```bash +docker run -d -p 3000:3000 -p 8000:8000 \ + -v surfsense-data:/data \ + # Discord Connector + -e DISCORD_CLIENT_ID=your_discord_client_id \ + -e DISCORD_CLIENT_SECRET=your_discord_client_secret \ + -e DISCORD_REDIRECT_URI=http://localhost:8000/api/v1/auth/discord/connector/callback \ + -e DISCORD_BOT_TOKEN=http://localhost:8000/api/v1/auth/discord/connector/callback \ + --name surfsense \ + --restart unless-stopped \ + ghcr.io/modsetter/surfsense:latest +``` diff --git a/surfsense_web/content/docs/connectors/linear.mdx b/surfsense_web/content/docs/connectors/linear.mdx index 20b31cabc..f9dc9a62b 100644 --- a/surfsense_web/content/docs/connectors/linear.mdx +++ b/surfsense_web/content/docs/connectors/linear.mdx @@ -3,31 +3,65 @@ title: Linear description: Connect your Linear workspace to SurfSense --- -# Linear Connector +# Linear OAuth Integration Setup Guide -Sync your Linear issues, projects, and documentation to SurfSense. +This guide walks you through setting up a Linear OAuth integration for SurfSense. -## Prerequisites +## Step 1: Access Linear API Settings -- A Linear account -- Access to the teams you want to connect +1. Navigate to your workspace's API settings at `linear.app//settings/api` +2. Under **OAuth Applications**, click **"+ New OAuth application"** -## Setup +![Linear API Settings Page](/docs/connectors/linear/linear-api-settings.png) -1. Navigate to your Search Space settings -2. Click on **Add Connector** -3. Select **Linear** from the list -4. Authorize SurfSense to access your Linear workspace -5. Select the teams and projects you want to index +## Step 2: Create New Application -## What Gets Indexed +Fill in the application details: -- Issues and sub-issues -- Issue descriptions and comments -- Project documentation -- Roadmap items +| Field | Value | +|-------|-------| +| **Application icon** | Upload an icon (at least 256x256px) | +| **Application name** | `SurfSense` | +| **Developer name** | `SurfSense` | +| **Developer URL** | `https://www.surfsense.com/` | +| **Description** | Connect any LLM to your internal knowledge sources and chat with it in real time alongside your team. | +| **Callback URLs** | `http://localhost:8000/api/v1/auth/linear/connector/callback` | +| **GitHub username** | Your GitHub username (optional) | -## Sync Frequency +### Settings -The Linear connector supports scheduled syncing to keep your content up to date. +- ✅ **Public** - Enable this to allow the application to be installed by other workspaces + +Click **Create** to create the application. + +![Create New Application Form](/docs/connectors/linear/linear-new-application.png) + +## Step 3: Get OAuth Credentials + +After creating the application, you'll see your OAuth credentials: + +1. Copy your **Client ID** +2. Copy your **Client Secret** + +> ⚠️ Never share your client secret publicly. + +![OAuth Credentials](/docs/connectors/linear/linear-oauth-credentials.png) + +--- + +## Running SurfSense with Linear Connector + +Add the Linear environment variables to your Docker run command: + +```bash +docker run -d -p 3000:3000 -p 8000:8000 \ + -v surfsense-data:/data \ + # Linear Connector + -e LINEAR_CLIENT_ID=your_linear_client_id \ + -e LINEAR_CLIENT_SECRET=your_linear_client_secret \ + -e LINEAR_REDIRECT_URI=http://localhost:8000/api/v1/auth/linear/connector/callback \ + --name surfsense \ + --restart unless-stopped \ + ghcr.io/modsetter/surfsense:latest +``` diff --git a/surfsense_web/content/docs/connectors/notion.mdx b/surfsense_web/content/docs/connectors/notion.mdx index 784875e3c..936972f7e 100644 --- a/surfsense_web/content/docs/connectors/notion.mdx +++ b/surfsense_web/content/docs/connectors/notion.mdx @@ -3,31 +3,82 @@ title: Notion description: Connect your Notion workspaces to SurfSense --- -# Notion Connector +# Notion OAuth Integration Setup Guide -Connect your Notion workspaces to index pages, databases, and content. +This guide walks you through setting up a Notion OAuth integration for SurfSense. -## Prerequisites +## Step 1: Access Notion Integrations -- A Notion account -- Access to the workspaces you want to connect +1. Navigate to [notion.so/profile/integrations](https://notion.so/profile/integrations) +2. Click the **"New integration"** button -## Setup +![Notion Integrations Page](/docs/connectors/notion/notion-integrations-page.png) -1. Navigate to your Search Space settings -2. Click on **Add Connector** -3. Select **Notion** from the list -4. Authorize SurfSense to access your Notion workspace -5. Select the pages and databases you want to index +## Step 2: Configure New Integration -## What Gets Indexed +Fill in the integration details: -- Pages and subpages -- Database entries -- Comments and discussions -- Embedded content +| Field | Value | +|-------|-------| +| **Integration Name** | `SurfSense` | +| **Associated workspace** | Select your workspace | +| **Type** | `Public` | +| **Company name** | Your company name | +| **Website** | Your website URL | +| **Tagline** | Brief description | +| **Privacy Policy URL** | Your privacy policy URL | +| **Terms of Use URL** | Your terms of use URL | +| **Email** | Your developer email | +| **Logo** | Upload a 512x512 logo | -## Sync Frequency +### OAuth Redirect URI -The Notion connector supports scheduled syncing to keep your content up to date. +Under **OAuth domains & URIs**, set the **Redirect URI** to: +``` +http://localhost:8000/api/v1/auth/notion/connector/callback +``` + +Click **Save** to create the integration. + +![New Integration Form](/docs/connectors/notion/notion-new-integration-form.png) + +## Step 3: Get OAuth Credentials & Configure Capabilities + +After creating the integration, you'll see the configuration page with your credentials: + +1. Copy your **OAuth Client ID** +2. Copy your **OAuth Client Secret** (click Refresh if needed) + +### Set Required Capabilities + +Under **Content Capabilities**, enable: +- ✅ Read content + +Under **Comment Capabilities**, enable: +- ✅ Read comments + +Under **User Capabilities**, select: +- 🔘 Read user information including email addresses + +Click **Save** to apply the capabilities. + +![Integration Configuration](/docs/connectors/notion/notion-integration-config.png) + +--- + +## Running SurfSense with Notion Connector + +Add the Notion environment variables to your Docker run command: + +```bash +docker run -d -p 3000:3000 -p 8000:8000 \ + -v surfsense-data:/data \ + # Notion Connector + -e NOTION_OAUTH_CLIENT_ID=your_notion_client_id \ + -e NOTION_OAUTH_CLIENT_SECRET=your_notion_client_secret \ + -e NOTION_REDIRECT_URI=http://localhost:8000/api/v1/auth/notion/connector/callback \ + --name surfsense \ + --restart unless-stopped \ + ghcr.io/modsetter/surfsense:latest +``` diff --git a/surfsense_web/content/docs/connectors/slack.mdx b/surfsense_web/content/docs/connectors/slack.mdx index 089ccf67d..838408cd7 100644 --- a/surfsense_web/content/docs/connectors/slack.mdx +++ b/surfsense_web/content/docs/connectors/slack.mdx @@ -3,31 +3,89 @@ title: Slack description: Connect your Slack workspace to SurfSense --- -# Slack Connector +# Slack OAuth Integration Setup Guide -Index your Slack conversations and make them searchable. +This guide walks you through setting up a Slack OAuth integration for SurfSense. -## Prerequisites +## Step 1: Create a New Slack App -- A Slack workspace -- Admin permissions to install apps +1. Navigate to [api.slack.com/apps](https://api.slack.com/apps) +2. Click **"Create New App"** +3. Select **"From scratch"** to manually configure your app -## Setup +![Create an App Dialog](/docs/connectors/slack/slack-create-app.png) -1. Navigate to your Search Space settings -2. Click on **Add Connector** -3. Select **Slack** from the list -4. Authorize SurfSense to access your Slack workspace -5. Select the channels you want to index +## Step 2: Name App & Choose Workspace -## What Gets Indexed +1. Enter **App Name**: `SurfSense` +2. Select the workspace to develop your app in +3. Click **"Create App"** -- Public channel messages -- Private channels (if authorized) -- Thread replies -- Shared files and links +> ⚠️ You won't be able to change the workspace later. The workspace will control the app even if you leave it. -## Sync Frequency +![Name App & Choose Workspace](/docs/connectors/slack/slack-name-workspace.png) -The Slack connector supports scheduled syncing to keep your conversations indexed. +## Step 3: Get App Credentials +After creating the app, you'll be taken to the **Basic Information** page. Here you'll find your credentials: + +1. Copy your **Client ID** +2. Copy your **Client Secret** (click Show to reveal) + +> ⚠️ Never share your app credentials publicly or include them in code repositories. + +![Basic Information - App Credentials](/docs/connectors/slack/slack-app-credentials.png) + +## Step 4: Configure Redirect URLs + +1. In the left sidebar, click **"OAuth & Permissions"** +2. Scroll down to **Redirect URLs** +3. Click **"Add New Redirect URL"** +4. Enter: `https://localhost:8000/api/v1/auth/slack/connector/callback` +5. Click **"Add"**, then **"Save URLs"** + +![Redirect URLs Configuration](/docs/connectors/slack/slack-redirect-urls.png) + +## Step 5: Configure Bot Token Scopes + +On the same **OAuth & Permissions** page, scroll to **Scopes** and add the following **Bot Token Scopes**: + +| OAuth Scope | Description | +|-------------|-------------| +| `channels:history` | View messages and other content in public channels | +| `channels:read` | View basic information about public channels | +| `groups:history` | View messages and other content in private channels | +| `groups:read` | View basic information about private channels | +| `im:history` | View messages and other content in direct messages | +| `mpim:history` | View messages and other content in group direct messages | +| `users:read` | View people in a workspace | + +Click **"Add an OAuth Scope"** to add each scope. + +![Bot Token Scopes](/docs/connectors/slack/slack-scopes.png) + +## Step 6: Enable Public Distribution + +1. In the left sidebar, click **"Manage Distribution"** +2. Under **Share Your App with Other Workspaces**, ensure distribution is enabled +3. You can use the **"Add to Slack"** button or **Sharable URL** to install the app + +![Manage Distribution](/docs/connectors/slack/slack-distribution.png) + +--- + +## Running SurfSense with Slack Connector + +Add the Slack environment variables to your Docker run command: + +```bash +docker run -d -p 3000:3000 -p 8000:8000 \ + -v surfsense-data:/data \ + # Slack Connector + -e SLACK_CLIENT_ID=your_slack_client_id \ + -e SLACK_CLIENT_SECRET=your_slack_client_secret \ + -e SLACK_REDIRECT_URI=https://localhost:8000/api/v1/auth/slack/connector/callback \ + --name surfsense \ + --restart unless-stopped \ + ghcr.io/modsetter/surfsense:latest +``` diff --git a/surfsense_web/content/docs/index.mdx b/surfsense_web/content/docs/index.mdx index 42f863176..e5f89621e 100644 --- a/surfsense_web/content/docs/index.mdx +++ b/surfsense_web/content/docs/index.mdx @@ -17,13 +17,13 @@ To set up Google OAuth: - **People API** (required for basic Google OAuth) - **Gmail API** (required if you want to use the Gmail connector) - **Google Calendar API** (required if you want to use the Google Calendar connector) -![Google Developer Console People API](/docs/google_oauth_people_api.png) +![Google Developer Console People API](/docs/connectors/google/google_oauth_people_api.png) 3. Set up OAuth consent screen. -![Google Developer Console OAuth consent screen](/docs/google_oauth_screen.png) +![Google Developer Console OAuth consent screen](/docs/connectors/google/google_oauth_screen.png) 4. Create OAuth client ID and secret. -![Google Developer Console OAuth client ID](/docs/google_oauth_client.png) +![Google Developer Console OAuth client ID](/docs/connectors/google/google_oauth_client.png) 5. It should look like this. -![Google Developer Console Config](/docs/google_oauth_config.png) +![Google Developer Console Config](/docs/connectors/google/google_oauth_config.png) --- diff --git a/surfsense_web/mdx-components.tsx b/surfsense_web/mdx-components.tsx index cea62b834..f6d86e543 100644 --- a/surfsense_web/mdx-components.tsx +++ b/surfsense_web/mdx-components.tsx @@ -7,12 +7,17 @@ import { AccordionTrigger, } from "@/components/ui/accordion"; import { cn } from "@/lib/utils"; +import Image, { type ImageProps } from "next/image"; export function getMDXComponents(components?: MDXComponents): MDXComponents { return { ...defaultMdxComponents, - img: ({ className, ...props }: React.ComponentProps<"img">) => ( - + img: ({ className, alt, ...props }: React.ComponentProps<"img">) => ( + {alt ), Video: ({ className, ...props }: React.ComponentProps<"video">) => (

- {connector.name} + {getConnectorDisplayName(connector.name)}

{isIndexing ? (

diff --git a/surfsense_web/components/assistant-ui/connector-popup/tabs/all-connectors-tab.tsx b/surfsense_web/components/assistant-ui/connector-popup/tabs/all-connectors-tab.tsx index bdec4dcb2..0be4e7e87 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/tabs/all-connectors-tab.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/tabs/all-connectors-tab.tsx @@ -1,12 +1,27 @@ "use client"; +import { Plus } from "lucide-react"; import type { FC } from "react"; +import { Button } from "@/components/ui/button"; import type { SearchSourceConnector } from "@/contracts/types/connector.types"; import type { LogActiveTask, LogSummary } from "@/contracts/types/log.types"; import { ConnectorCard } from "../components/connector-card"; import { CRAWLERS, OAUTH_CONNECTORS, OTHER_CONNECTORS } from "../constants/connector-constants"; import { getDocumentCountForConnector } from "../utils/connector-document-mapping"; +/** + * Extract the display name from a full connector name. + * Full names are in format "Base Name - identifier" (e.g., "Gmail - john@example.com"). + * Returns just the identifier (e.g : john@example.com). + */ +export function getConnectorDisplayName(fullName: string): string { + const separatorIndex = fullName.indexOf(" - "); + if (separatorIndex !== -1) { + return fullName.substring(separatorIndex + 3); + } + return fullName; +} + interface AllConnectorsTabProps { searchQuery: string; searchSpaceId: string; @@ -67,56 +82,78 @@ export const AllConnectorsTab: FC = ({ return (

- {/* Quick Connect */} - {filteredOAuth.length > 0 && ( -
-
-

Quick Connect

-
-
- {filteredOAuth.map((connector) => { - const isConnected = connectedTypes.has(connector.connectorType); - const isConnecting = connectingId === connector.id; - // Find the actual connector object if connected - const actualConnector = - isConnected && allConnectors - ? allConnectors.find( - (c: SearchSourceConnector) => c.connector_type === connector.connectorType - ) - : undefined; + {/* Per-Type OAuth Connector Groups */} + {filteredOAuth.map((connectorType) => { + const userConnectors = + allConnectors?.filter( + (c: SearchSourceConnector) => c.connector_type === connectorType.connectorType + ) || []; + const isConnecting = connectingId === connectorType.id; - const documentCount = getDocumentCountForConnector( - connector.connectorType, - documentTypeCounts - ); - const isIndexing = actualConnector && indexingConnectorIds?.has(actualConnector.id); - const activeTask = actualConnector - ? getActiveTaskForConnector(actualConnector.id) - : undefined; + return ( +
+ {/* Group Header */} +
+

+ {connectorType.title} Integrations +

+ {userConnectors.length > 0 && ( + + )} +
- return ( +
+ {userConnectors.length === 0 ? ( onConnectOAuth(connector)} - onManage={ - actualConnector && onManage ? () => onManage(actualConnector) : undefined - } + onConnect={() => onConnectOAuth(connectorType)} /> - ); - })} -
-
- )} + ) : ( + userConnectors.map((connector: SearchSourceConnector) => { + const documentCount = getDocumentCountForConnector( + connector.connector_type, + documentTypeCounts + ); + const isIndexing = indexingConnectorIds?.has(connector.id); + const activeTask = getActiveTaskForConnector(connector.id); + + return ( + onConnectOAuth(connectorType)} + onManage={onManage ? () => onManage(connector) : undefined} + /> + ); + }) + )} +
+
+ ); + })} {/* More Integrations */} {filteredOther.length > 0 && ( From 761fa9162b7af956da1ca0fb9699721bbc1c15e4 Mon Sep 17 00:00:00 2001 From: "DESKTOP-RTLN3BA\\$punk" Date: Tue, 6 Jan 2026 22:18:52 -0800 Subject: [PATCH 53/75] feat: added atlassian docs --- .../content/docs/connectors/airtable.mdx | 29 +---- .../content/docs/connectors/bookstack.mdx | 29 +---- .../content/docs/connectors/circleback.mdx | 8 ++ .../content/docs/connectors/clickup.mdx | 29 +---- .../content/docs/connectors/confluence.mdx | 110 +++++++++++++++--- .../content/docs/connectors/elasticsearch.mdx | 28 +---- .../content/docs/connectors/github.mdx | 30 +---- .../content/docs/connectors/jira.mdx | 97 ++++++++++++--- .../content/docs/connectors/luma.mdx | 29 +---- .../content/docs/connectors/meta.json | 20 ++-- .../content/docs/connectors/web-crawler.mdx | 34 +----- .../atlassian/atlassian-authorization.png | Bin 0 -> 70536 bytes .../atlassian/atlassian-create-app.png | Bin 0 -> 75666 bytes .../atlassian-dev-console-access.png | Bin 0 -> 133645 bytes .../atlassian/atlassian-name-integration.png | Bin 0 -> 60665 bytes .../atlassian/atlassian-permissions.png | Bin 0 -> 85471 bytes .../atlassian-confluence-classic-scopes.png | Bin 0 -> 90542 bytes .../atlassian-confluence-granular-scopes.png | Bin 0 -> 97286 bytes .../atlassian/jira/atlassian-jira-scopes.png | Bin 0 -> 93282 bytes 19 files changed, 194 insertions(+), 249 deletions(-) create mode 100644 surfsense_web/content/docs/connectors/circleback.mdx create mode 100644 surfsense_web/public/docs/connectors/atlassian/atlassian-authorization.png create mode 100644 surfsense_web/public/docs/connectors/atlassian/atlassian-create-app.png create mode 100644 surfsense_web/public/docs/connectors/atlassian/atlassian-dev-console-access.png create mode 100644 surfsense_web/public/docs/connectors/atlassian/atlassian-name-integration.png create mode 100644 surfsense_web/public/docs/connectors/atlassian/atlassian-permissions.png create mode 100644 surfsense_web/public/docs/connectors/atlassian/confluence/atlassian-confluence-classic-scopes.png create mode 100644 surfsense_web/public/docs/connectors/atlassian/confluence/atlassian-confluence-granular-scopes.png create mode 100644 surfsense_web/public/docs/connectors/atlassian/jira/atlassian-jira-scopes.png diff --git a/surfsense_web/content/docs/connectors/airtable.mdx b/surfsense_web/content/docs/connectors/airtable.mdx index 647208271..1fbe427ec 100644 --- a/surfsense_web/content/docs/connectors/airtable.mdx +++ b/surfsense_web/content/docs/connectors/airtable.mdx @@ -3,31 +3,4 @@ title: Airtable description: Connect your Airtable bases to SurfSense --- -# Airtable Connector - -Index your Airtable bases, tables, and records. - -## Prerequisites - -- An Airtable account -- API access to the bases you want to connect - -## Setup - -1. Navigate to your Search Space settings -2. Click on **Add Connector** -3. Select **Airtable** from the list -4. Enter your Airtable API key -5. Select the bases and tables you want to index - -## What Gets Indexed - -- Table records -- Field values -- Attachments -- Linked records - -## Sync Frequency - -The Airtable connector supports scheduled syncing to keep your data up to date. - +# Documentation in progress diff --git a/surfsense_web/content/docs/connectors/bookstack.mdx b/surfsense_web/content/docs/connectors/bookstack.mdx index c05a6376b..8ee581948 100644 --- a/surfsense_web/content/docs/connectors/bookstack.mdx +++ b/surfsense_web/content/docs/connectors/bookstack.mdx @@ -3,31 +3,4 @@ title: Bookstack description: Connect your Bookstack instance to SurfSense --- -# Bookstack Connector - -Index your Bookstack books, chapters, and pages. - -## Prerequisites - -- A Bookstack instance -- API access credentials - -## Setup - -1. Navigate to your Search Space settings -2. Click on **Add Connector** -3. Select **Bookstack** from the list -4. Enter your Bookstack instance URL and API credentials -5. Select the shelves and books you want to index - -## What Gets Indexed - -- Books and chapters -- Pages and content -- Attachments -- Tags and metadata - -## Sync Frequency - -The Bookstack connector supports scheduled syncing to keep your content up to date. - +# Documentation in progress \ No newline at end of file diff --git a/surfsense_web/content/docs/connectors/circleback.mdx b/surfsense_web/content/docs/connectors/circleback.mdx new file mode 100644 index 000000000..a5c90a28f --- /dev/null +++ b/surfsense_web/content/docs/connectors/circleback.mdx @@ -0,0 +1,8 @@ +--- +title: Circleback +description: Connect your circleback to SurfSense +--- + +# Documentation in progress + + diff --git a/surfsense_web/content/docs/connectors/clickup.mdx b/surfsense_web/content/docs/connectors/clickup.mdx index 90734555a..f59030788 100644 --- a/surfsense_web/content/docs/connectors/clickup.mdx +++ b/surfsense_web/content/docs/connectors/clickup.mdx @@ -3,31 +3,4 @@ title: ClickUp description: Connect your ClickUp workspace to SurfSense --- -# ClickUp Connector - -Sync your ClickUp tasks, docs, and content to SurfSense. - -## Prerequisites - -- A ClickUp account -- Access to the workspaces you want to connect - -## Setup - -1. Navigate to your Search Space settings -2. Click on **Add Connector** -3. Select **ClickUp** from the list -4. Authorize SurfSense to access your ClickUp workspace -5. Select the spaces and folders you want to index - -## What Gets Indexed - -- Tasks and subtasks -- Task descriptions and comments -- ClickUp Docs -- Custom fields - -## Sync Frequency - -The ClickUp connector supports scheduled syncing to keep your content up to date. - +# Documentation in progress \ No newline at end of file diff --git a/surfsense_web/content/docs/connectors/confluence.mdx b/surfsense_web/content/docs/connectors/confluence.mdx index 17643a86d..aa220fcbe 100644 --- a/surfsense_web/content/docs/connectors/confluence.mdx +++ b/surfsense_web/content/docs/connectors/confluence.mdx @@ -3,32 +3,104 @@ title: Confluence description: Connect your Confluence spaces to SurfSense --- -# Confluence Connector +# Confluence OAuth Integration Setup Guide -Index your Confluence pages, spaces, and documentation. +This guide walks you through setting up an Atlassian OAuth 2.0 (3LO) integration for SurfSense to connect your Confluence spaces. -## Prerequisites +## Step 1: Access the Developer Console -- A Confluence account (Cloud or Data Center) -- Access to the spaces you want to connect +1. Navigate to [developer.atlassian.com](https://developer.atlassian.com) +2. Click your profile icon in the top-right corner +3. Select **"Developer console"** from the dropdown -## Setup +![Atlassian Developer Console Access](/docs/connectors/atlassian/atlassian-dev-console-access.png) -1. Navigate to your Search Space settings -2. Click on **Add Connector** -3. Select **Confluence** from the list -4. Enter your Confluence instance URL and credentials -5. Select the spaces you want to index +## Step 2: Create a New OAuth 2.0 Integration -## What Gets Indexed +1. In the Developer Console, under **My apps**, click the **"Create"** button +2. Select **"OAuth 2.0 integration"** from the dropdown -- Pages and blog posts -- Page comments -- Attachments -- Space documentation -- Page hierarchy +![Create OAuth 2.0 Integration](/docs/connectors/atlassian/atlassian-create-app.png) -## Sync Frequency +## Step 3: Name Your Integration -The Confluence connector supports scheduled syncing to keep your content up to date. +1. Enter **Name**: `SurfSense` +2. Check the box to agree to Atlassian's developer terms +3. Click **"Create"** +> ℹ️ New OAuth 2.0 integrations use rotating refresh tokens, which improve security by limiting token validity and enabling automatic detection of token reuse. + +![Create New Integration Form](/docs/connectors/atlassian/atlassian-name-integration.png) + +## Step 4: Configure Callback URL + +1. In the left sidebar, click **"Authorization"** +2. Under **Callback URLs**, enter the redirect URI: + ``` + http://localhost:8000/api/v1/auth/confluence/connector/callback + ``` +3. Click **"Save changes"** + +> ℹ️ You can enter up to 10 redirect URIs, one per line. + +![Authorization Callback URLs](/docs/connectors/atlassian/atlassian-authorization.png) + +## Step 5: Configure API Permissions + +1. In the left sidebar, click **"Permissions"** +2. You'll see a list of available APIs including Confluence API + +![Permissions Overview](/docs/connectors/atlassian/atlassian-permissions.png) + +## Step 6: Configure Confluence API Scopes + +1. Click **"Configure"** next to **Confluence API** + +### Classic Scopes + +Select the **"Classic scopes"** tab and enable: + +| Scope Name | Code | Description | +|------------|------|-------------| +| ✅ Read user | `read:confluence-user` | View user information in Confluence that you have access to, including usernames, email addresses and profile pictures | + +![Confluence API Classic Scopes](/docs/connectors/atlassian/confluence/atlassian-confluence-classic-scopes.png) + +### Granular Scopes + +Select the **"Granular scopes"** tab and enable: + +| Scope Name | Code | Description | +|------------|------|-------------| +| ✅ View pages | `read:page:confluence` | View page content | +| ✅ View comments | `read:comment:confluence` | View comments on pages or blogposts | +| ✅ View spaces | `read:space:confluence` | View space details | + +4. Click **"Save"** + +![Confluence API Granular Scopes](/docs/connectors/atlassian/confluence/atlassian-confluence-granular-scopes.png) + +## Step 7: Get OAuth Credentials + +1. In the left sidebar, click **"Settings"** +2. Copy your **Client ID** and **Client Secret** + +> ⚠️ Never share your client secret publicly or include it in code repositories. + +--- + +## Running SurfSense with Confluence Connector + +Add the Atlassian environment variables to your Docker run command: + +```bash +docker run -d -p 3000:3000 -p 8000:8000 \ + -v surfsense-data:/data \ + # Confluence Connector + -e ATLASSIAN_CLIENT_ID=your_atlassian_client_id \ + -e ATLASSIAN_CLIENT_SECRET=your_atlassian_client_secret \ + -e CONFLUENCE_REDIRECT_URI=http://localhost:8000/api/v1/auth/confluence/connector/callback \ + --name surfsense \ + --restart unless-stopped \ + ghcr.io/modsetter/surfsense:latest +``` diff --git a/surfsense_web/content/docs/connectors/elasticsearch.mdx b/surfsense_web/content/docs/connectors/elasticsearch.mdx index 8fb003253..ac43cca4e 100644 --- a/surfsense_web/content/docs/connectors/elasticsearch.mdx +++ b/surfsense_web/content/docs/connectors/elasticsearch.mdx @@ -3,30 +3,4 @@ title: Elasticsearch description: Connect your Elasticsearch cluster to SurfSense --- -# Elasticsearch Connector - -Index data from your Elasticsearch cluster. - -## Prerequisites - -- An Elasticsearch cluster -- Access credentials - -## Setup - -1. Navigate to your Search Space settings -2. Click on **Add Connector** -3. Select **Elasticsearch** from the list -4. Enter your Elasticsearch cluster URL and credentials -5. Configure the indices you want to index - -## What Gets Indexed - -- Documents from specified indices -- Custom field mappings -- Metadata - -## Sync Frequency - -The Elasticsearch connector supports scheduled syncing to keep your data up to date. - +# Documentation in progress \ No newline at end of file diff --git a/surfsense_web/content/docs/connectors/github.mdx b/surfsense_web/content/docs/connectors/github.mdx index 90bb91a96..bb2faca81 100644 --- a/surfsense_web/content/docs/connectors/github.mdx +++ b/surfsense_web/content/docs/connectors/github.mdx @@ -3,32 +3,4 @@ title: GitHub description: Connect your GitHub repositories to SurfSense --- -# GitHub Connector - -Index your GitHub repositories, issues, pull requests, and documentation. - -## Prerequisites - -- A GitHub account -- Access to the repositories you want to connect - -## Setup - -1. Navigate to your Search Space settings -2. Click on **Add Connector** -3. Select **GitHub** from the list -4. Authorize SurfSense to access your GitHub account -5. Select the repositories you want to index - -## What Gets Indexed - -- Repository README and documentation -- Issues and issue comments -- Pull requests and PR comments -- Code files (configurable) -- Discussions - -## Sync Frequency - -The GitHub connector supports scheduled syncing to keep your content up to date. - +# Documentation in progress \ No newline at end of file diff --git a/surfsense_web/content/docs/connectors/jira.mdx b/surfsense_web/content/docs/connectors/jira.mdx index 9c086d24b..9d00a56af 100644 --- a/surfsense_web/content/docs/connectors/jira.mdx +++ b/surfsense_web/content/docs/connectors/jira.mdx @@ -3,32 +3,91 @@ title: Jira description: Connect your Jira projects to SurfSense --- -# Jira Connector +# Jira OAuth Integration Setup Guide -Sync your Jira issues, projects, and documentation to SurfSense. +This guide walks you through setting up an Atlassian OAuth 2.0 (3LO) integration for SurfSense to connect your Jira projects. -## Prerequisites +## Step 1: Access the Developer Console -- A Jira account (Cloud or Data Center) -- Access to the projects you want to connect +1. Navigate to [developer.atlassian.com](https://developer.atlassian.com) +2. Click your profile icon in the top-right corner +3. Select **"Developer console"** from the dropdown -## Setup +![Atlassian Developer Console Access](/docs/connectors/atlassian/atlassian-dev-console-access.png) -1. Navigate to your Search Space settings -2. Click on **Add Connector** -3. Select **Jira** from the list -4. Enter your Jira instance URL and credentials -5. Select the projects you want to index +## Step 2: Create a New OAuth 2.0 Integration -## What Gets Indexed +1. In the Developer Console, under **My apps**, click the **"Create"** button +2. Select **"OAuth 2.0 integration"** from the dropdown -- Issues and subtasks -- Issue descriptions and comments -- Attachments -- Custom fields -- Project documentation +![Create OAuth 2.0 Integration](/docs/connectors/atlassian/atlassian-create-app.png) -## Sync Frequency +## Step 3: Name Your Integration -The Jira connector supports scheduled syncing to keep your content up to date. +1. Enter **Name**: `SurfSense` +2. Check the box to agree to Atlassian's developer terms +3. Click **"Create"** +> ℹ️ New OAuth 2.0 integrations use rotating refresh tokens, which improve security by limiting token validity and enabling automatic detection of token reuse. + +![Create New Integration Form](/docs/connectors/atlassian/atlassian-name-integration.png) + +## Step 4: Configure Callback URL + +1. In the left sidebar, click **"Authorization"** +2. Under **Callback URLs**, enter the redirect URI: + ``` + http://localhost:8000/api/v1/auth/jira/connector/callback + ``` +3. Click **"Save changes"** + +> ℹ️ You can enter up to 10 redirect URIs, one per line. + +![Authorization Callback URLs](/docs/connectors/atlassian/atlassian-authorization.png) + +## Step 5: Configure API Permissions + +1. In the left sidebar, click **"Permissions"** +2. You'll see a list of available APIs including Jira API + +![Permissions Overview](/docs/connectors/atlassian/atlassian-permissions.png) + +## Step 6: Configure Jira API Scopes + +1. Click **"Configure"** next to **Jira API** +2. Select the **"Classic scopes"** tab +3. Under **Jira platform REST API**, select the following scopes: + +| Scope Name | Code | Description | +|------------|------|-------------| +| ✅ View Jira issue data | `read:jira-work` | Read Jira project and issue data, search for issues, and objects associated with issues like attachments and worklogs | +| ✅ View user profiles | `read:jira-user` | View user information in Jira that the user has access to, including usernames, email addresses, and avatars | + +4. Click **"Save"** + +![Jira API Scopes](/docs/connectors/atlassian/jira/atlassian-jira-scopes.png) + +## Step 7: Get OAuth Credentials + +1. In the left sidebar, click **"Settings"** +2. Copy your **Client ID** and **Client Secret** + +> ⚠️ Never share your client secret publicly or include it in code repositories. + +--- + +## Running SurfSense with Jira Connector + +Add the Atlassian environment variables to your Docker run command: + +```bash +docker run -d -p 3000:3000 -p 8000:8000 \ + -v surfsense-data:/data \ + # Jira Connector + -e ATLASSIAN_CLIENT_ID=your_atlassian_client_id \ + -e ATLASSIAN_CLIENT_SECRET=your_atlassian_client_secret \ + -e JIRA_REDIRECT_URI=http://localhost:8000/api/v1/auth/jira/connector/callback \ + --name surfsense \ + --restart unless-stopped \ + ghcr.io/modsetter/surfsense:latest +``` diff --git a/surfsense_web/content/docs/connectors/luma.mdx b/surfsense_web/content/docs/connectors/luma.mdx index 8a244df07..e16e5a949 100644 --- a/surfsense_web/content/docs/connectors/luma.mdx +++ b/surfsense_web/content/docs/connectors/luma.mdx @@ -3,31 +3,4 @@ title: Luma description: Connect your Luma events to SurfSense --- -# Luma Connector - -Index your Luma events and event content. - -## Prerequisites - -- A Luma account -- API access - -## Setup - -1. Navigate to your Search Space settings -2. Click on **Add Connector** -3. Select **Luma** from the list -4. Authorize SurfSense to access your Luma account -5. Select the events you want to index - -## What Gets Indexed - -- Event details and descriptions -- Event schedules -- Attendee information (if authorized) -- Event updates - -## Sync Frequency - -The Luma connector supports scheduled syncing to keep your events up to date. - +# Documentation in progress \ No newline at end of file diff --git a/surfsense_web/content/docs/connectors/meta.json b/surfsense_web/content/docs/connectors/meta.json index 2515bc7d8..70635c6b3 100644 --- a/surfsense_web/content/docs/connectors/meta.json +++ b/surfsense_web/content/docs/connectors/meta.json @@ -1,22 +1,22 @@ { "title": "Connectors", "pages": [ - "notion", - "slack", - "discord", - "clickup", - "github", - "jira", - "linear", "google-drive", "gmail", "google-calendar", + "notion", + "slack", + "discord", + "jira", + "linear", "confluence", - "bookstack", "airtable", + "clickup", + "github", + "luma", + "circleback", "elasticsearch", - "web-crawler", - "luma" + "bookstack" ], "defaultOpen": true } diff --git a/surfsense_web/content/docs/connectors/web-crawler.mdx b/surfsense_web/content/docs/connectors/web-crawler.mdx index 2a23dea1a..6ea5b1c2b 100644 --- a/surfsense_web/content/docs/connectors/web-crawler.mdx +++ b/surfsense_web/content/docs/connectors/web-crawler.mdx @@ -3,36 +3,4 @@ title: Web Crawler description: Crawl and index websites with SurfSense --- -# Web Crawler Connector - -Crawl and index public websites to make them searchable. - -## Prerequisites - -- Firecrawl API key (see [Prerequisites](/docs)) - -## Setup - -1. Navigate to your Search Space settings -2. Click on **Add Connector** -3. Select **Web Crawler** from the list -4. Enter the URL(s) you want to crawl -5. Configure crawl depth and settings - -## What Gets Indexed - -- Web page content -- Page titles and metadata -- Links and navigation -- Images and media (configurable) - -## Configuration Options - -- **Crawl Depth**: How many levels deep to crawl -- **Include/Exclude Patterns**: Filter which URLs to index -- **Rate Limiting**: Control crawl speed - -## Sync Frequency - -The Web Crawler connector supports scheduled re-crawling to keep your content up to date. - +# Documentation in progress \ No newline at end of file diff --git a/surfsense_web/public/docs/connectors/atlassian/atlassian-authorization.png b/surfsense_web/public/docs/connectors/atlassian/atlassian-authorization.png new file mode 100644 index 0000000000000000000000000000000000000000..71c63e200e8d225fd03d42e8d12813fed03cbc39 GIT binary patch literal 70536 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

$H2dChmbJugj)vuyb7_TyJTF*Kb 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

p>11V@!Xc;-E9J%6deSMi|nx+`{@mc}@ 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 N$M=V_?~ncbKLFweU%>zX literal 0 HcmV?d00001 diff --git a/surfsense_web/public/docs/connectors/atlassian/atlassian-create-app.png b/surfsense_web/public/docs/connectors/atlassian/atlassian-create-app.png new file mode 100644 index 0000000000000000000000000000000000000000..5a7fa7dede6ee50f31d898010cf75b418168d284 GIT binary patch literal 75666 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

oq(!!f)H(?q9YBaxZPm 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 literal 0 HcmV?d00001 diff --git a/surfsense_web/public/docs/connectors/atlassian/atlassian-dev-console-access.png b/surfsense_web/public/docs/connectors/atlassian/atlassian-dev-console-access.png new file mode 100644 index 0000000000000000000000000000000000000000..cdfd0926d07a93db04a15c8487dae8eb1e624002 GIT binary patch literal 133645 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 literal 0 HcmV?d00001 diff --git a/surfsense_web/public/docs/connectors/atlassian/atlassian-name-integration.png b/surfsense_web/public/docs/connectors/atlassian/atlassian-name-integration.png new file mode 100644 index 0000000000000000000000000000000000000000..ebfcb7211d4a9b3be58c1eeab1f8eadcfc0aab12 GIT binary patch literal 60665 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 oer)dkJ2dAJ81LQvd(} literal 0 HcmV?d00001 diff --git a/surfsense_web/public/docs/connectors/atlassian/atlassian-permissions.png b/surfsense_web/public/docs/connectors/atlassian/atlassian-permissions.png new file mode 100644 index 0000000000000000000000000000000000000000..98df14e6f005fe46182c05bfffb0b8cce2651f37 GIT binary patch literal 85471 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 j8lcy)3~C+83mA%-~h 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 AAOHXW literal 0 HcmV?d00001 diff --git a/surfsense_web/public/docs/connectors/atlassian/confluence/atlassian-confluence-classic-scopes.png b/surfsense_web/public/docs/connectors/atlassian/confluence/atlassian-confluence-classic-scopes.png new file mode 100644 index 0000000000000000000000000000000000000000..99eb2d6d7dd05cdd10e96227de00be728e6e89f0 GIT binary patch literal 90542 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

    GX17pd^CDgWqN{NNOrH1L$Q8wXP`lJ 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%0Asl3`?*5Me8xV%auo!V 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 literal 0 HcmV?d00001 diff --git a/surfsense_web/public/docs/connectors/atlassian/confluence/atlassian-confluence-granular-scopes.png b/surfsense_web/public/docs/connectors/atlassian/confluence/atlassian-confluence-granular-scopes.png new file mode 100644 index 0000000000000000000000000000000000000000..67363023249069cb7ecbcc7cfaddf216e746bcec GIT binary patch literal 97286 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%0A<8U>Bw)V}7^hXiDli^a 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 literal 0 HcmV?d00001 diff --git a/surfsense_web/public/docs/connectors/atlassian/jira/atlassian-jira-scopes.png b/surfsense_web/public/docs/connectors/atlassian/jira/atlassian-jira-scopes.png new file mode 100644 index 0000000000000000000000000000000000000000..28bb42bd55c30c9a9dfccaef4e61e4942789370c GIT binary patch literal 93282 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%0D=$!0GPikjL^?*z^% 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 literal 0 HcmV?d00001 From 755f92323a9303c4052a4c1fead34e126f9e0475 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Wed, 7 Jan 2026 08:36:55 +0200 Subject: [PATCH 54/75] fix: connector card and edit view styling --- .../connector-popup/components/connector-card.tsx | 6 +++--- .../connector-configs/views/connector-edit-view.tsx | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/surfsense_web/components/assistant-ui/connector-popup/components/connector-card.tsx b/surfsense_web/components/assistant-ui/connector-popup/components/connector-card.tsx index 855be95a2..faf20e055 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/components/connector-card.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/components/connector-card.tsx @@ -139,7 +139,7 @@ export const ConnectorCard: FC = ({ return (

    -
    +
    {connectorType ? ( getConnectorIcon(connectorType, "size-6") ) : id === "youtube-crawler" ? ( @@ -150,7 +150,7 @@ export const ConnectorCard: FC = ({
    - {title} + {title}
    {getStatusContent()}
    {isConnected && documentCount !== undefined && ( @@ -163,7 +163,7 @@ export const ConnectorCard: FC = ({ size="sm" variant={isConnected ? "secondary" : "default"} className={cn( - "h-8 text-[11px] px-3 rounded-lg flex-shrink-0 font-medium", + "h-8 text-[11px] px-3 rounded-lg shrink-0 font-medium", isConnected && "bg-white text-slate-700 hover:bg-slate-50 border-0 shadow-xs dark:bg-secondary dark:text-secondary-foreground dark:hover:bg-secondary/80", !isConnected && "shadow-xs" diff --git a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-edit-view.tsx b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-edit-view.tsx index 7776c9a9d..e09bdea90 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-edit-view.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-edit-view.tsx @@ -143,12 +143,12 @@ export const ConnectorEditView: FC = ({ {/* Connector header */}
    -
    -
    +
    +
    {getConnectorIcon(connector.connector_type, "size-7")}
    -

    {connector.name}

    +

    {connector.name}

    Manage your connector settings and sync configuration

    From 1b4ec2daa71a9d4e4bbb7bda0610600ef6ae559b Mon Sep 17 00:00:00 2001 From: "DESKTOP-RTLN3BA\\$punk" Date: Tue, 6 Jan 2026 23:20:44 -0800 Subject: [PATCH 55/75] feat: added google based connectors docs --- .../content/docs/connectors/gmail.mdx | 87 +++++++++++++++---- .../docs/connectors/google-calendar.mdx | 86 ++++++++++++++---- .../content/docs/connectors/google-drive.mdx | 87 +++++++++++++++---- surfsense_web/content/docs/index.mdx | 2 - 4 files changed, 203 insertions(+), 59 deletions(-) diff --git a/surfsense_web/content/docs/connectors/gmail.mdx b/surfsense_web/content/docs/connectors/gmail.mdx index ac5486ce6..6c08804fc 100644 --- a/surfsense_web/content/docs/connectors/gmail.mdx +++ b/surfsense_web/content/docs/connectors/gmail.mdx @@ -3,32 +3,81 @@ title: Gmail description: Connect your Gmail to SurfSense --- -# Gmail Connector +# Gmail OAuth Integration Setup Guide -Index your Gmail emails and make them searchable. +This guide walks you through setting up a Google OAuth 2.0 integration for SurfSense to connect your Gmail account. -## Prerequisites +## Step 1: Access the Google Cloud Console -- A Google account -- Google OAuth configured in SurfSense (see [Prerequisites](/docs)) -- Gmail API enabled in Google Cloud Console +1. Navigate to [Google Cloud Console](https://console.cloud.google.com/) +2. Select an existing project or create a new one -## Setup +## Step 2: Enable Required APIs -1. Navigate to your Search Space settings -2. Click on **Add Connector** -3. Select **Gmail** from the list -4. Authorize SurfSense to access your Gmail -5. Configure which labels/folders to index +1. Go to **APIs & Services** > **Library** +2. Search for and enable the following APIs: + - **People API** (required for Google OAuth) + - **Gmail API** (required for Gmail connector) -## What Gets Indexed +![Google Developer Console People API](/docs/connectors/google/google_oauth_people_api.png) -- Email content -- Email attachments -- Thread conversations -- Labels and categories +## Step 3: Configure OAuth Consent Screen -## Sync Frequency +1. Go to **APIs & Services** > **OAuth consent screen** +2. Select **External** user type (or Internal if using Google Workspace) +3. Fill in the required information: + - **App name**: `SurfSense` + - **User support email**: Your email address + - **Developer contact information**: Your email address +4. Click **Save and Continue** -The Gmail connector supports scheduled syncing to keep your emails indexed. +![Google Developer Console OAuth consent screen](/docs/connectors/google/google_oauth_screen.png) +### Add Scopes + +1. Click **Add or Remove Scopes** +2. Add the following scopes: + - `https://www.googleapis.com/auth/gmail.readonly` - Read Gmail messages + - `https://www.googleapis.com/auth/userinfo.email` - View user email address +3. Click **Update** and then **Save and Continue** + +## Step 4: Create OAuth Client ID + +1. Go to **APIs & Services** > **Credentials** +2. Click **Create Credentials** > **OAuth client ID** +3. Select **Web application** as the application type +4. Enter **Name**: `SurfSense` +5. Under **Authorized redirect URIs**, add: + ``` + http://localhost:8000/api/v1/auth/google/gmail/connector/callback + ``` +6. Click **Create** + +![Google Developer Console OAuth client ID](/docs/connectors/google/google_oauth_client.png) + +## Step 5: Get OAuth Credentials + +1. After creating the OAuth client, you'll see a dialog with your credentials +2. Copy your **Client ID** and **Client Secret** + +> ⚠️ Never share your client secret publicly or include it in code repositories. + +![Google Developer Console Config](/docs/connectors/google/google_oauth_config.png) + +--- + +## Running SurfSense with Gmail Connector + +Add the Google OAuth environment variables to your Docker run command: + +```bash +docker run -d -p 3000:3000 -p 8000:8000 \ + -v surfsense-data:/data \ + # Gmail Connector + -e GOOGLE_OAUTH_CLIENT_ID=your_google_client_id \ + -e GOOGLE_OAUTH_CLIENT_SECRET=your_google_client_secret \ + -e GOOGLE_GMAIL_REDIRECT_URI=http://localhost:8000/api/v1/auth/google/gmail/connector/callback \ + --name surfsense \ + --restart unless-stopped \ + ghcr.io/modsetter/surfsense:latest +``` diff --git a/surfsense_web/content/docs/connectors/google-calendar.mdx b/surfsense_web/content/docs/connectors/google-calendar.mdx index 76b3ea588..e6ae4d593 100644 --- a/surfsense_web/content/docs/connectors/google-calendar.mdx +++ b/surfsense_web/content/docs/connectors/google-calendar.mdx @@ -3,32 +3,80 @@ title: Google Calendar description: Connect your Google Calendar to SurfSense --- -# Google Calendar Connector +# Google Calendar OAuth Integration Setup Guide -Index your Google Calendar events and make them searchable. +This guide walks you through setting up a Google OAuth 2.0 integration for SurfSense to connect your Google Calendar. -## Prerequisites +## Step 1: Access the Google Cloud Console -- A Google account -- Google OAuth configured in SurfSense (see [Prerequisites](/docs)) -- Google Calendar API enabled in Google Cloud Console +1. Navigate to [Google Cloud Console](https://console.cloud.google.com/) +2. Select an existing project or create a new one -## Setup +## Step 2: Enable Required APIs -1. Navigate to your Search Space settings -2. Click on **Add Connector** -3. Select **Google Calendar** from the list -4. Authorize SurfSense to access your Google Calendar -5. Select which calendars to index +1. Go to **APIs & Services** > **Library** +2. Search for and enable the following APIs: + - **People API** (required for Google OAuth) + - **Google Calendar API** (required for Calendar connector) -## What Gets Indexed +![Google Developer Console People API](/docs/connectors/google/google_oauth_people_api.png) -- Event titles and descriptions -- Event attendees -- Meeting notes -- Recurring events +## Step 3: Configure OAuth Consent Screen -## Sync Frequency +1. Go to **APIs & Services** > **OAuth consent screen** +2. Select **External** user type (or Internal if using Google Workspace) +3. Fill in the required information: + - **App name**: `SurfSense` + - **User support email**: Your email address + - **Developer contact information**: Your email address +4. Click **Save and Continue** -The Google Calendar connector supports scheduled syncing to keep your events indexed. +![Google Developer Console OAuth consent screen](/docs/connectors/google/google_oauth_screen.png) +### Add Scopes + +1. Click **Add or Remove Scopes** +2. Add the following scope: + - `https://www.googleapis.com/auth/calendar.readonly` - Read Google Calendar events +3. Click **Update** and then **Save and Continue** + +## Step 4: Create OAuth Client ID + +1. Go to **APIs & Services** > **Credentials** +2. Click **Create Credentials** > **OAuth client ID** +3. Select **Web application** as the application type +4. Enter **Name**: `SurfSense` +5. Under **Authorized redirect URIs**, add: + ``` + http://localhost:8000/api/v1/auth/google/calendar/connector/callback + ``` +6. Click **Create** + +![Google Developer Console OAuth client ID](/docs/connectors/google/google_oauth_client.png) + +## Step 5: Get OAuth Credentials + +1. After creating the OAuth client, you'll see a dialog with your credentials +2. Copy your **Client ID** and **Client Secret** + +> ⚠️ Never share your client secret publicly or include it in code repositories. + +![Google Developer Console Config](/docs/connectors/google/google_oauth_config.png) + +--- + +## Running SurfSense with Google Calendar Connector + +Add the Google OAuth environment variables to your Docker run command: + +```bash +docker run -d -p 3000:3000 -p 8000:8000 \ + -v surfsense-data:/data \ + # Google Calendar Connector + -e GOOGLE_OAUTH_CLIENT_ID=your_google_client_id \ + -e GOOGLE_OAUTH_CLIENT_SECRET=your_google_client_secret \ + -e GOOGLE_CALENDAR_REDIRECT_URI=http://localhost:8000/api/v1/auth/google/calendar/connector/callback \ + --name surfsense \ + --restart unless-stopped \ + ghcr.io/modsetter/surfsense:latest +``` diff --git a/surfsense_web/content/docs/connectors/google-drive.mdx b/surfsense_web/content/docs/connectors/google-drive.mdx index 6538e24b5..f2b0105fc 100644 --- a/surfsense_web/content/docs/connectors/google-drive.mdx +++ b/surfsense_web/content/docs/connectors/google-drive.mdx @@ -3,32 +3,81 @@ title: Google Drive description: Connect your Google Drive to SurfSense --- -# Google Drive Connector +# Google Drive OAuth Integration Setup Guide -Index your Google Drive files, documents, and shared content. +This guide walks you through setting up a Google OAuth 2.0 integration for SurfSense to connect your Google Drive. -## Prerequisites +## Step 1: Access the Google Cloud Console -- A Google account -- Google OAuth configured in SurfSense (see [Prerequisites](/docs)) +1. Navigate to [Google Cloud Console](https://console.cloud.google.com/) +2. Select an existing project or create a new one -## Setup +## Step 2: Enable Required APIs -1. Navigate to your Search Space settings -2. Click on **Add Connector** -3. Select **Google Drive** from the list -4. Authorize SurfSense to access your Google Drive -5. Select the folders you want to index +1. Go to **APIs & Services** > **Library** +2. Search for and enable the following APIs: + - **People API** (required for Google OAuth) + - **Google Drive API** (required for Drive connector) -## What Gets Indexed +![Google Developer Console People API](/docs/connectors/google/google_oauth_people_api.png) -- Google Docs -- Google Sheets -- Google Slides -- PDFs and other documents -- Shared files +## Step 3: Configure OAuth Consent Screen -## Sync Frequency +1. Go to **APIs & Services** > **OAuth consent screen** +2. Select **External** user type (or Internal if using Google Workspace) +3. Fill in the required information: + - **App name**: `SurfSense` + - **User support email**: Your email address + - **Developer contact information**: Your email address +4. Click **Save and Continue** -The Google Drive connector supports scheduled syncing to keep your content up to date. +![Google Developer Console OAuth consent screen](/docs/connectors/google/google_oauth_screen.png) +### Add Scopes + +1. Click **Add or Remove Scopes** +2. Add the following scopes: + - `https://www.googleapis.com/auth/drive.readonly` - Read-only access to Google Drive + - `https://www.googleapis.com/auth/userinfo.email` - View user email address +3. Click **Update** and then **Save and Continue** + +## Step 4: Create OAuth Client ID + +1. Go to **APIs & Services** > **Credentials** +2. Click **Create Credentials** > **OAuth client ID** +3. Select **Web application** as the application type +4. Enter **Name**: `SurfSense` +5. Under **Authorized redirect URIs**, add: + ``` + http://localhost:8000/api/v1/auth/google/drive/connector/callback + ``` +6. Click **Create** + +![Google Developer Console OAuth client ID](/docs/connectors/google/google_oauth_client.png) + +## Step 5: Get OAuth Credentials + +1. After creating the OAuth client, you'll see a dialog with your credentials +2. Copy your **Client ID** and **Client Secret** + +> ⚠️ Never share your client secret publicly or include it in code repositories. + +![Google Developer Console Config](/docs/connectors/google/google_oauth_config.png) + +--- + +## Running SurfSense with Google Drive Connector + +Add the Google OAuth environment variables to your Docker run command: + +```bash +docker run -d -p 3000:3000 -p 8000:8000 \ + -v surfsense-data:/data \ + # Google Drive Connector + -e GOOGLE_OAUTH_CLIENT_ID=your_google_client_id \ + -e GOOGLE_OAUTH_CLIENT_SECRET=your_google_client_secret \ + -e GOOGLE_DRIVE_REDIRECT_URI=http://localhost:8000/api/v1/auth/google/drive/connector/callback \ + --name surfsense \ + --restart unless-stopped \ + ghcr.io/modsetter/surfsense:latest +``` diff --git a/surfsense_web/content/docs/index.mdx b/surfsense_web/content/docs/index.mdx index e5f89621e..bb07c5f68 100644 --- a/surfsense_web/content/docs/index.mdx +++ b/surfsense_web/content/docs/index.mdx @@ -15,8 +15,6 @@ To set up Google OAuth: 1. Login to your [Google Developer Console](https://console.cloud.google.com/) 2. Enable the required APIs: - **People API** (required for basic Google OAuth) - - **Gmail API** (required if you want to use the Gmail connector) - - **Google Calendar API** (required if you want to use the Google Calendar connector) ![Google Developer Console People API](/docs/connectors/google/google_oauth_people_api.png) 3. Set up OAuth consent screen. ![Google Developer Console OAuth consent screen](/docs/connectors/google/google_oauth_screen.png) From 2508b37f4ef7c576d41722e0d0d3e5c6850a7a32 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Wed, 7 Jan 2026 09:28:07 +0200 Subject: [PATCH 56/75] feat: add connector accounts list view for OAuth connectors with multiple accounts - Create ConnectorAccountsListView component to show all connected accounts for a connector type - Add state management in use-connector-dialog for viewing connector accounts list - Update AllConnectorsTab to show accounts list when OAuth connector is connected - Update connector-popup.tsx to render the new accounts list view - Add 'accounts' view to connector popup URL schema - Display connected accounts in 2-column grid layout - Add 'Add Account' button with dashed border in header --- .../assistant-ui/connector-popup.tsx | 27 + .../constants/connector-popup.schemas.ts | 2 +- .../hooks/use-connector-dialog.ts | 100 +++- .../tabs/all-connectors-tab.tsx | 498 +++++++++--------- .../views/connector-accounts-list-view.tsx | 214 ++++++++ 5 files changed, 595 insertions(+), 246 deletions(-) create mode 100644 surfsense_web/components/assistant-ui/connector-popup/views/connector-accounts-list-view.tsx diff --git a/surfsense_web/components/assistant-ui/connector-popup.tsx b/surfsense_web/components/assistant-ui/connector-popup.tsx index 8fb1e7652..c5e996c4c 100644 --- a/surfsense_web/components/assistant-ui/connector-popup.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup.tsx @@ -19,9 +19,11 @@ import { ConnectorDialogHeader } from "./connector-popup/components/connector-di import { ConnectorConnectView } from "./connector-popup/connector-configs/views/connector-connect-view"; import { ConnectorEditView } from "./connector-popup/connector-configs/views/connector-edit-view"; import { IndexingConfigurationView } from "./connector-popup/connector-configs/views/indexing-configuration-view"; +import { OAUTH_CONNECTORS } from "./connector-popup/constants/connector-constants"; import { useConnectorDialog } from "./connector-popup/hooks/use-connector-dialog"; import { ActiveConnectorsTab } from "./connector-popup/tabs/active-connectors-tab"; import { AllConnectorsTab } from "./connector-popup/tabs/all-connectors-tab"; +import { ConnectorAccountsListView } from "./connector-popup/views/connector-accounts-list-view"; import { YouTubeCrawlerView } from "./connector-popup/views/youtube-crawler-view"; export const ConnectorIndicator: FC = () => { @@ -60,6 +62,7 @@ export const ConnectorIndicator: FC = () => { periodicEnabled, frequencyMinutes, allConnectors, + viewingAccountsType, setSearchQuery, setStartDate, setEndDate, @@ -81,6 +84,9 @@ export const ConnectorIndicator: FC = () => { handleBackFromEdit, handleBackFromConnect, handleBackFromYouTube, + handleViewAccountsList, + handleBackFromAccountsList, + handleAddAccountOAuth, handleQuickIndexConnector, connectorConfig, setConnectorConfig, @@ -193,6 +199,26 @@ export const ConnectorIndicator: FC = () => { {/* YouTube Crawler View - shown when adding YouTube videos */} {isYouTubeView && searchSpaceId ? ( + ) : viewingAccountsType ? ( + { + const oauthConnector = OAUTH_CONNECTORS.find( + (c) => c.connectorType === viewingAccountsType.connectorType + ); + if (oauthConnector) { + handleAddAccountOAuth(oauthConnector); + } + }} + isConnecting={connectingId !== null} + /> ) : connectingConnectorType ? ( { onCreateWebcrawler={handleCreateWebcrawler} onCreateYouTubeCrawler={handleCreateYouTubeCrawler} onManage={handleStartEdit} + onViewAccountsList={handleViewAccountsList} /> diff --git a/surfsense_web/components/assistant-ui/connector-popup/constants/connector-popup.schemas.ts b/surfsense_web/components/assistant-ui/connector-popup/constants/connector-popup.schemas.ts index 65456689c..808c7b428 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/constants/connector-popup.schemas.ts +++ b/surfsense_web/components/assistant-ui/connector-popup/constants/connector-popup.schemas.ts @@ -7,7 +7,7 @@ import { searchSourceConnectorTypeEnum } from "@/contracts/types/connector.types export const connectorPopupQueryParamsSchema = z.object({ modal: z.enum(["connectors"]).optional(), tab: z.enum(["all", "active"]).optional(), - view: z.enum(["configure", "edit", "connect", "youtube"]).optional(), + view: z.enum(["configure", "edit", "connect", "youtube", "accounts"]).optional(), connector: z.string().optional(), connectorId: z.string().optional(), connectorType: z.string().optional(), diff --git a/surfsense_web/components/assistant-ui/connector-popup/hooks/use-connector-dialog.ts b/surfsense_web/components/assistant-ui/connector-popup/hooks/use-connector-dialog.ts index 8ddaa973a..a9d4871e1 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/hooks/use-connector-dialog.ts +++ b/surfsense_web/components/assistant-ui/connector-popup/hooks/use-connector-dialog.ts @@ -66,6 +66,12 @@ export const useConnectorDialog = () => { const [isCreatingConnector, setIsCreatingConnector] = useState(false); const isCreatingConnectorRef = useRef(false); + // Accounts list view state (for OAuth connectors with multiple accounts) + const [viewingAccountsType, setViewingAccountsType] = useState<{ + connectorType: string; + connectorTitle: string; + } | null>(null); + // Helper function to get frequency label const getFrequencyLabel = useCallback((minutes: string): string => { switch (minutes) { @@ -114,11 +120,29 @@ export const useConnectorDialog = () => { setConnectingConnectorType(null); } + // Clear viewing accounts type if view is not "accounts" anymore + if (params.view !== "accounts" && viewingAccountsType) { + setViewingAccountsType(null); + } + // Handle connect view if (params.view === "connect" && params.connectorType && !connectingConnectorType) { setConnectingConnectorType(params.connectorType); } + // Handle accounts view + if (params.view === "accounts" && params.connectorType && !viewingAccountsType) { + const oauthConnector = OAUTH_CONNECTORS.find( + (c) => c.connectorType === params.connectorType + ); + if (oauthConnector) { + setViewingAccountsType({ + connectorType: oauthConnector.connectorType, + connectorTitle: oauthConnector.title, + }); + } + } + // Handle YouTube view if (params.view === "youtube") { // YouTube view is active - no additional state needed @@ -200,6 +224,10 @@ export const useConnectorDialog = () => { if (connectingConnectorType) { setConnectingConnectorType(null); } + // Clear viewing accounts type when modal is closed + if (viewingAccountsType) { + setViewingAccountsType(null); + } // Clear YouTube view when modal is closed (handled by view param check) } } catch (error) { @@ -207,7 +235,7 @@ export const useConnectorDialog = () => { console.warn("Invalid connector popup query params:", error); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [searchParams, allConnectors, editingConnector, indexingConfig, connectingConnectorType]); + }, [searchParams, allConnectors, editingConnector, indexingConfig, connectingConnectorType, viewingAccountsType]); // Detect OAuth success and transition to config view useEffect(() => { @@ -632,6 +660,71 @@ export const useConnectorDialog = () => { router.replace(url.pathname + url.search, { scroll: false }); }, [router]); + // Handle viewing accounts list for OAuth connector type + const handleViewAccountsList = useCallback( + (connector: (typeof OAUTH_CONNECTORS)[number]) => { + if (!searchSpaceId) return; + + setViewingAccountsType({ + connectorType: connector.connectorType, + connectorTitle: connector.title, + }); + + // Update URL to show accounts view + const url = new URL(window.location.href); + url.searchParams.set("modal", "connectors"); + url.searchParams.set("view", "accounts"); + url.searchParams.set("connectorType", connector.connectorType); + window.history.pushState({ modal: true }, "", url.toString()); + }, + [searchSpaceId] + ); + + // Handle going back from accounts list view + const handleBackFromAccountsList = useCallback(() => { + setViewingAccountsType(null); + const url = new URL(window.location.href); + url.searchParams.set("modal", "connectors"); + url.searchParams.set("tab", "all"); + url.searchParams.delete("view"); + url.searchParams.delete("connectorType"); + router.replace(url.pathname + url.search, { scroll: false }); + }, [router]); + + // Handle adding a new account for OAuth connector (from accounts list view) + const handleAddAccountOAuth = useCallback( + async (connector: (typeof OAUTH_CONNECTORS)[number]) => { + if (!searchSpaceId || !connector.authEndpoint) return; + + // Set connecting state + setConnectingId(connector.id); + + try { + const response = await authenticatedFetch( + `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}${connector.authEndpoint}?space_id=${searchSpaceId}`, + { method: "GET" } + ); + + if (!response.ok) { + throw new Error(`Failed to initiate ${connector.title} OAuth`); + } + + const data = await response.json(); + const validatedData = parseOAuthAuthResponse(data); + window.location.href = validatedData.auth_url; + } catch (error) { + console.error(`Error connecting to ${connector.title}:`, error); + if (error instanceof Error && error.message.includes("Invalid auth URL")) { + toast.error(`Invalid response from ${connector.title} OAuth endpoint`); + } else { + toast.error(`Failed to connect to ${connector.title}`); + } + setConnectingId(null); + } + }, + [searchSpaceId] + ); + // Handle starting indexing const handleStartIndexing = useCallback( async (refreshConnectors: () => void) => { @@ -1081,6 +1174,7 @@ export const useConnectorDialog = () => { setConnectorName(null); setConnectorConfig(null); setConnectingConnectorType(null); + setViewingAccountsType(null); setStartDate(undefined); setEndDate(undefined); setPeriodicEnabled(false); @@ -1126,6 +1220,7 @@ export const useConnectorDialog = () => { frequencyMinutes, searchSpaceId, allConnectors, + viewingAccountsType, // Setters setSearchQuery, @@ -1152,6 +1247,9 @@ export const useConnectorDialog = () => { handleBackFromEdit, handleBackFromConnect, handleBackFromYouTube, + handleViewAccountsList, + handleBackFromAccountsList, + handleAddAccountOAuth, handleQuickIndexConnector, connectorConfig, setConnectorConfig, diff --git a/surfsense_web/components/assistant-ui/connector-popup/tabs/all-connectors-tab.tsx b/surfsense_web/components/assistant-ui/connector-popup/tabs/all-connectors-tab.tsx index 0be4e7e87..5356a2afd 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/tabs/all-connectors-tab.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/tabs/all-connectors-tab.tsx @@ -6,7 +6,11 @@ import { Button } from "@/components/ui/button"; import type { SearchSourceConnector } from "@/contracts/types/connector.types"; import type { LogActiveTask, LogSummary } from "@/contracts/types/log.types"; import { ConnectorCard } from "../components/connector-card"; -import { CRAWLERS, OAUTH_CONNECTORS, OTHER_CONNECTORS } from "../constants/connector-constants"; +import { + CRAWLERS, + OAUTH_CONNECTORS, + OTHER_CONNECTORS, +} from "../constants/connector-constants"; import { getDocumentCountForConnector } from "../utils/connector-document-mapping"; /** @@ -15,271 +19,277 @@ import { getDocumentCountForConnector } from "../utils/connector-document-mappin * Returns just the identifier (e.g : john@example.com). */ export function getConnectorDisplayName(fullName: string): string { - const separatorIndex = fullName.indexOf(" - "); - if (separatorIndex !== -1) { - return fullName.substring(separatorIndex + 3); - } - return fullName; + const separatorIndex = fullName.indexOf(" - "); + if (separatorIndex !== -1) { + return fullName.substring(separatorIndex + 3); + } + return fullName; } interface AllConnectorsTabProps { - searchQuery: string; - searchSpaceId: string; - connectedTypes: Set; - connectingId: string | null; - allConnectors: SearchSourceConnector[] | undefined; - documentTypeCounts?: Record; - indexingConnectorIds?: Set; - logsSummary?: LogSummary; - onConnectOAuth: (connector: (typeof OAUTH_CONNECTORS)[number]) => void; - onConnectNonOAuth?: (connectorType: string) => void; - onCreateWebcrawler?: () => void; - onCreateYouTubeCrawler?: () => void; - onManage?: (connector: SearchSourceConnector) => void; + searchQuery: string; + searchSpaceId: string; + connectedTypes: Set; + connectingId: string | null; + allConnectors: SearchSourceConnector[] | undefined; + documentTypeCounts?: Record; + indexingConnectorIds?: Set; + logsSummary?: LogSummary; + onConnectOAuth: (connector: (typeof OAUTH_CONNECTORS)[number]) => void; + onConnectNonOAuth?: (connectorType: string) => void; + onCreateWebcrawler?: () => void; + onCreateYouTubeCrawler?: () => void; + onManage?: (connector: SearchSourceConnector) => void; + onViewAccountsList?: (connector: (typeof OAUTH_CONNECTORS)[number]) => void; } export const AllConnectorsTab: FC = ({ - searchQuery, - searchSpaceId, - connectedTypes, - connectingId, - allConnectors, - documentTypeCounts, - indexingConnectorIds, - logsSummary, - onConnectOAuth, - onConnectNonOAuth, - onCreateWebcrawler, - onCreateYouTubeCrawler, - onManage, + searchQuery, + searchSpaceId, + connectedTypes, + connectingId, + allConnectors, + documentTypeCounts, + indexingConnectorIds, + logsSummary, + onConnectOAuth, + onConnectNonOAuth, + onCreateWebcrawler, + onCreateYouTubeCrawler, + onManage, + onViewAccountsList, }) => { - // Helper to find active task for a connector - const getActiveTaskForConnector = (connectorId: number): LogActiveTask | undefined => { - if (!logsSummary?.active_tasks) return undefined; - return logsSummary.active_tasks.find( - (task: LogActiveTask) => task.connector_id === connectorId - ); - }; + // Helper to find active task for a connector + const getActiveTaskForConnector = ( + connectorId: number + ): LogActiveTask | undefined => { + if (!logsSummary?.active_tasks) return undefined; + return logsSummary.active_tasks.find( + (task: LogActiveTask) => task.connector_id === connectorId + ); + }; - // Filter connectors based on search - const filteredOAuth = OAUTH_CONNECTORS.filter( - (c) => - c.title.toLowerCase().includes(searchQuery.toLowerCase()) || - c.description.toLowerCase().includes(searchQuery.toLowerCase()) - ); + // Filter connectors based on search + const filteredOAuth = OAUTH_CONNECTORS.filter( + (c) => + c.title.toLowerCase().includes(searchQuery.toLowerCase()) || + c.description.toLowerCase().includes(searchQuery.toLowerCase()) + ); - const filteredCrawlers = CRAWLERS.filter( - (c) => - c.title.toLowerCase().includes(searchQuery.toLowerCase()) || - c.description.toLowerCase().includes(searchQuery.toLowerCase()) - ); + const filteredCrawlers = CRAWLERS.filter( + (c) => + c.title.toLowerCase().includes(searchQuery.toLowerCase()) || + c.description.toLowerCase().includes(searchQuery.toLowerCase()) + ); - const filteredOther = OTHER_CONNECTORS.filter( - (c) => - c.title.toLowerCase().includes(searchQuery.toLowerCase()) || - c.description.toLowerCase().includes(searchQuery.toLowerCase()) - ); + const filteredOther = OTHER_CONNECTORS.filter( + (c) => + c.title.toLowerCase().includes(searchQuery.toLowerCase()) || + c.description.toLowerCase().includes(searchQuery.toLowerCase()) + ); - return ( -
    - {/* Per-Type OAuth Connector Groups */} - {filteredOAuth.map((connectorType) => { - const userConnectors = - allConnectors?.filter( - (c: SearchSourceConnector) => c.connector_type === connectorType.connectorType - ) || []; - const isConnecting = connectingId === connectorType.id; + return ( +
    + {/* Quick Connect */} + {filteredOAuth.length > 0 && ( +
    +
    +

    + Quick Connect +

    +
    +
    + {filteredOAuth.map((connector) => { + const isConnected = connectedTypes.has(connector.connectorType); + const isConnecting = connectingId === connector.id; + // Find the actual connector object if connected + const actualConnector = + isConnected && allConnectors + ? allConnectors.find( + (c: SearchSourceConnector) => + c.connector_type === connector.connectorType + ) + : undefined; - return ( -
    - {/* Group Header */} -
    -

    - {connectorType.title} Integrations -

    - {userConnectors.length > 0 && ( - - )} -
    + const documentCount = getDocumentCountForConnector( + connector.connectorType, + documentTypeCounts + ); + const isIndexing = + actualConnector && + indexingConnectorIds?.has(actualConnector.id); + const activeTask = actualConnector + ? getActiveTaskForConnector(actualConnector.id) + : undefined; -
    - {userConnectors.length === 0 ? ( - onConnectOAuth(connectorType)} - /> - ) : ( - userConnectors.map((connector: SearchSourceConnector) => { - const documentCount = getDocumentCountForConnector( - connector.connector_type, - documentTypeCounts - ); - const isIndexing = indexingConnectorIds?.has(connector.id); - const activeTask = getActiveTaskForConnector(connector.id); + return ( + onConnectOAuth(connector)} + onManage={ + isConnected && onViewAccountsList + ? () => onViewAccountsList(connector) + : undefined + } + /> + ); + })} +
    +
    + )} - return ( - onConnectOAuth(connectorType)} - onManage={onManage ? () => onManage(connector) : undefined} - /> - ); - }) - )} -
    -
    - ); - })} + {/* More Integrations */} + {filteredOther.length > 0 && ( +
    +
    +

    + More Integrations +

    +
    +
    + {filteredOther.map((connector) => { + const isConnected = connectedTypes.has(connector.connectorType); + const isConnecting = connectingId === connector.id; - {/* More Integrations */} - {filteredOther.length > 0 && ( -
    -
    -

    More Integrations

    -
    -
    - {filteredOther.map((connector) => { - const isConnected = connectedTypes.has(connector.connectorType); - const isConnecting = connectingId === connector.id; + // Find the actual connector object if connected + const actualConnector = + isConnected && allConnectors + ? allConnectors.find( + (c: SearchSourceConnector) => + c.connector_type === connector.connectorType + ) + : undefined; - // Find the actual connector object if connected - const actualConnector = - isConnected && allConnectors - ? allConnectors.find( - (c: SearchSourceConnector) => c.connector_type === connector.connectorType - ) - : undefined; + const documentCount = getDocumentCountForConnector( + connector.connectorType, + documentTypeCounts + ); + const isIndexing = + actualConnector && + indexingConnectorIds?.has(actualConnector.id); + const activeTask = actualConnector + ? getActiveTaskForConnector(actualConnector.id) + : undefined; - const documentCount = getDocumentCountForConnector( - connector.connectorType, - documentTypeCounts - ); - const isIndexing = actualConnector && indexingConnectorIds?.has(actualConnector.id); - const activeTask = actualConnector - ? getActiveTaskForConnector(actualConnector.id) - : undefined; + const handleConnect = onConnectNonOAuth + ? () => onConnectNonOAuth(connector.connectorType) + : () => {}; // Fallback - connector popup should handle all connector types - const handleConnect = onConnectNonOAuth - ? () => onConnectNonOAuth(connector.connectorType) - : () => {}; // Fallback - connector popup should handle all connector types + return ( + onManage(actualConnector) + : undefined + } + /> + ); + })} +
    +
    + )} - return ( - onManage(actualConnector) : undefined - } - /> - ); - })} -
    -
    - )} + {/* Content Sources */} + {filteredCrawlers.length > 0 && ( +
    +
    +

    + Content Sources +

    +
    +
    + {filteredCrawlers.map((crawler) => { + const isYouTube = crawler.id === "youtube-crawler"; + const isWebcrawler = crawler.id === "webcrawler-connector"; - {/* Content Sources */} - {filteredCrawlers.length > 0 && ( -
    -
    -

    Content Sources

    -
    -
    - {filteredCrawlers.map((crawler) => { - const isYouTube = crawler.id === "youtube-crawler"; - const isWebcrawler = crawler.id === "webcrawler-connector"; + // For crawlers that are actual connectors, check connection status + const isConnected = crawler.connectorType + ? connectedTypes.has(crawler.connectorType) + : false; + const isConnecting = connectingId === crawler.id; - // For crawlers that are actual connectors, check connection status - const isConnected = crawler.connectorType - ? connectedTypes.has(crawler.connectorType) - : false; - const isConnecting = connectingId === crawler.id; + // Find the actual connector object if connected + const actualConnector = + isConnected && crawler.connectorType && allConnectors + ? allConnectors.find( + (c: SearchSourceConnector) => + c.connector_type === crawler.connectorType + ) + : undefined; - // Find the actual connector object if connected - const actualConnector = - isConnected && crawler.connectorType && allConnectors - ? allConnectors.find( - (c: SearchSourceConnector) => c.connector_type === crawler.connectorType - ) - : undefined; + const documentCount = crawler.connectorType + ? getDocumentCountForConnector( + crawler.connectorType, + documentTypeCounts + ) + : undefined; + const isIndexing = + actualConnector && + indexingConnectorIds?.has(actualConnector.id); + const activeTask = actualConnector + ? getActiveTaskForConnector(actualConnector.id) + : undefined; - const documentCount = crawler.connectorType - ? getDocumentCountForConnector(crawler.connectorType, documentTypeCounts) - : undefined; - const isIndexing = actualConnector && indexingConnectorIds?.has(actualConnector.id); - const activeTask = actualConnector - ? getActiveTaskForConnector(actualConnector.id) - : undefined; + const handleConnect = + isYouTube && onCreateYouTubeCrawler + ? onCreateYouTubeCrawler + : isWebcrawler && onCreateWebcrawler + ? onCreateWebcrawler + : crawler.connectorType && onConnectNonOAuth + ? () => { + if (crawler.connectorType) { + onConnectNonOAuth(crawler.connectorType); + } + } + : () => {}; // Fallback for non-connector crawlers - const handleConnect = - isYouTube && onCreateYouTubeCrawler - ? onCreateYouTubeCrawler - : isWebcrawler && onCreateWebcrawler - ? onCreateWebcrawler - : crawler.connectorType && onConnectNonOAuth - ? () => { - if (crawler.connectorType) { - onConnectNonOAuth(crawler.connectorType); - } - } - : () => {}; // Fallback for non-connector crawlers - - return ( - onManage(actualConnector) : undefined - } - /> - ); - })} -
    -
    - )} -
    - ); + return ( + onManage(actualConnector) + : undefined + } + /> + ); + })} +
    + + )} +
    + ); }; diff --git a/surfsense_web/components/assistant-ui/connector-popup/views/connector-accounts-list-view.tsx b/surfsense_web/components/assistant-ui/connector-popup/views/connector-accounts-list-view.tsx new file mode 100644 index 000000000..23faedc4a --- /dev/null +++ b/surfsense_web/components/assistant-ui/connector-popup/views/connector-accounts-list-view.tsx @@ -0,0 +1,214 @@ +"use client"; + +import { differenceInDays, differenceInMinutes, format, isToday, isYesterday } from "date-fns"; +import { ArrowLeft, Loader2, Plus } from "lucide-react"; +import type { FC } from "react"; +import { Button } from "@/components/ui/button"; +import { getConnectorIcon } from "@/contracts/enums/connectorIcons"; +import type { SearchSourceConnector } from "@/contracts/types/connector.types"; +import type { LogActiveTask, LogSummary } from "@/contracts/types/log.types"; +import { cn } from "@/lib/utils"; +import { getDocumentCountForConnector } from "../utils/connector-document-mapping"; +import { getConnectorDisplayName } from "../tabs/all-connectors-tab"; + +interface ConnectorAccountsListViewProps { + connectorType: string; + connectorTitle: string; + connectors: SearchSourceConnector[]; + indexingConnectorIds: Set; + logsSummary: LogSummary | undefined; + documentTypeCounts?: Record; + onBack: () => void; + onManage: (connector: SearchSourceConnector) => void; + onAddAccount: () => void; + isConnecting?: boolean; +} + +/** + * Format document count (e.g., "1.2k docs", "500 docs", "1.5M docs") + */ +function formatDocumentCount(count: number | undefined): string { + if (count === undefined || count === 0) return "0 docs"; + if (count < 1000) return `${count} docs`; + if (count < 1000000) { + const k = (count / 1000).toFixed(1); + return `${k.replace(/\.0$/, "")}k docs`; + } + const m = (count / 1000000).toFixed(1); + return `${m.replace(/\.0$/, "")}M docs`; +} + +/** + * Format last indexed date with contextual messages + */ +function formatLastIndexedDate(dateString: string): string { + const date = new Date(dateString); + const now = new Date(); + const minutesAgo = differenceInMinutes(now, date); + const daysAgo = differenceInDays(now, date); + + if (minutesAgo < 1) { + return "Just now"; + } + + if (minutesAgo < 60) { + return `${minutesAgo} ${minutesAgo === 1 ? "minute" : "minutes"} ago`; + } + + if (isToday(date)) { + return `Today at ${format(date, "h:mm a")}`; + } + + if (isYesterday(date)) { + return `Yesterday at ${format(date, "h:mm a")}`; + } + + if (daysAgo < 7) { + return `${daysAgo} ${daysAgo === 1 ? "day" : "days"} ago`; + } + + return format(date, "MMM d, yyyy"); +} + +export const ConnectorAccountsListView: FC = ({ + connectorType, + connectorTitle, + connectors, + indexingConnectorIds, + logsSummary, + documentTypeCounts, + onBack, + onManage, + onAddAccount, + isConnecting = false, +}) => { + // Filter connectors to only show those of this type + const typeConnectors = connectors.filter((c) => c.connector_type === connectorType); + + return ( +
    + {/* Header */} +
    +
    +
    + +
    +
    + {getConnectorIcon(connectorType, "size-5")} +
    +
    +

    {connectorTitle} Accounts

    +

    + {typeConnectors.length} connected account{typeConnectors.length !== 1 ? "s" : ""} +

    +
    +
    +
    + {/* Add Account Button with dashed border */} + +
    +
    + + {/* Content */} +
    + {/* Connected Accounts Grid */} +
    + {typeConnectors.map((connector) => { + const isIndexing = indexingConnectorIds.has(connector.id); + const activeTask = logsSummary?.active_tasks?.find( + (task: LogActiveTask) => task.connector_id === connector.id + ); + const documentCount = getDocumentCountForConnector( + connector.connector_type, + documentTypeCounts + ); + + return ( +
    +
    + {getConnectorIcon(connector.connector_type, "size-6")} +
    +
    +

    + {getConnectorDisplayName(connector.name)} +

    + {isIndexing ? ( +

    + + Indexing... + {activeTask?.message && ( + + • {activeTask.message} + + )} +

    + ) : ( +

    + {connector.last_indexed_at + ? `Last indexed: ${formatLastIndexedDate(connector.last_indexed_at)}` + : "Never indexed"} +

    + )} +

    + {formatDocumentCount(documentCount)} +

    +
    + +
    + ); + })} +
    +
    +
    + ); +}; + From 9ad1348d6b1c48da49f0780eb181925d5a1a525b Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Wed, 7 Jan 2026 10:54:49 +0200 Subject: [PATCH 57/75] feat: add connectorId support for multi-account OAuth connectors Backend: - Add connectorId to OAuth redirect URLs in all 10 connector routes - Enables frontend to identify the specific connector created Frontend: - Update OAuth success handler to use connectorId for finding new connector - Set connectorId in URL when transitioning to configure view - Add connectorId support in URL sync effect for page refresh - Consolidate handleAddAccountOAuth into handleConnectOAuth - Update indexing config view to show connector type and display name --- .../routes/airtable_add_connector_route.py | 2 +- .../routes/confluence_add_connector_route.py | 2 +- .../app/routes/discord_add_connector_route.py | 2 +- .../google_calendar_add_connector_route.py | 2 +- .../google_drive_add_connector_route.py | 2 +- .../google_gmail_add_connector_route.py | 2 +- .../app/routes/jira_add_connector_route.py | 2 +- .../app/routes/linear_add_connector_route.py | 2 +- .../app/routes/notion_add_connector_route.py | 2 +- .../app/routes/slack_add_connector_route.py | 2 +- .../assistant-ui/connector-popup.tsx | 3 +- .../views/indexing-configuration-view.tsx | 16 +++-- .../hooks/use-connector-dialog.ts | 71 +++++++------------ 13 files changed, 48 insertions(+), 62 deletions(-) diff --git a/surfsense_backend/app/routes/airtable_add_connector_route.py b/surfsense_backend/app/routes/airtable_add_connector_route.py index 9632c9308..93a263ed0 100644 --- a/surfsense_backend/app/routes/airtable_add_connector_route.py +++ b/surfsense_backend/app/routes/airtable_add_connector_route.py @@ -332,7 +332,7 @@ async def airtable_callback( # Redirect to the frontend with success params for indexing config # Using query params to auto-open the popup with config view on new-chat page return RedirectResponse( - url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&success=true&connector=airtable-connector" + url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&success=true&connector=airtable-connector&connectorId={new_connector.id}" ) except ValidationError as e: diff --git a/surfsense_backend/app/routes/confluence_add_connector_route.py b/surfsense_backend/app/routes/confluence_add_connector_route.py index 7c2a0e2ca..284b4768a 100644 --- a/surfsense_backend/app/routes/confluence_add_connector_route.py +++ b/surfsense_backend/app/routes/confluence_add_connector_route.py @@ -324,7 +324,7 @@ async def confluence_callback( # Redirect to the frontend with success params return RedirectResponse( - url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&success=true&connector=confluence-connector" + url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&success=true&connector=confluence-connector&connectorId={new_connector.id}" ) except ValidationError as e: diff --git a/surfsense_backend/app/routes/discord_add_connector_route.py b/surfsense_backend/app/routes/discord_add_connector_route.py index d32902730..0bd864b89 100644 --- a/surfsense_backend/app/routes/discord_add_connector_route.py +++ b/surfsense_backend/app/routes/discord_add_connector_route.py @@ -320,7 +320,7 @@ async def discord_callback( # Redirect to the frontend with success params return RedirectResponse( - url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&success=true&connector=discord-connector" + url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&success=true&connector=discord-connector&connectorId={new_connector.id}" ) except ValidationError as e: 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 7210efae0..0770ec030 100644 --- a/surfsense_backend/app/routes/google_calendar_add_connector_route.py +++ b/surfsense_backend/app/routes/google_calendar_add_connector_route.py @@ -218,7 +218,7 @@ async def calendar_callback( # Redirect to the frontend with success params for indexing config # Using query params to auto-open the popup with config view on new-chat page return RedirectResponse( - f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&success=true&connector=google-calendar-connector" + f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&success=true&connector=google-calendar-connector&connectorId={db_connector.id}" ) except ValidationError as e: await session.rollback() diff --git a/surfsense_backend/app/routes/google_drive_add_connector_route.py b/surfsense_backend/app/routes/google_drive_add_connector_route.py index e63e4df30..ba45d7a2f 100644 --- a/surfsense_backend/app/routes/google_drive_add_connector_route.py +++ b/surfsense_backend/app/routes/google_drive_add_connector_route.py @@ -297,7 +297,7 @@ async def drive_callback( ) return RedirectResponse( - url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&success=true&connector=google-drive-connector" + url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&success=true&connector=google-drive-connector&connectorId={db_connector.id}" ) except HTTPException: diff --git a/surfsense_backend/app/routes/google_gmail_add_connector_route.py b/surfsense_backend/app/routes/google_gmail_add_connector_route.py index a6071ca53..6baeca83c 100644 --- a/surfsense_backend/app/routes/google_gmail_add_connector_route.py +++ b/surfsense_backend/app/routes/google_gmail_add_connector_route.py @@ -254,7 +254,7 @@ async def gmail_callback( # Redirect to the frontend with success params for indexing config # Using query params to auto-open the popup with config view on new-chat page return RedirectResponse( - url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&success=true&connector=google-gmail-connector" + url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&success=true&connector=google-gmail-connector&connectorId={db_connector.id}" ) except IntegrityError as e: diff --git a/surfsense_backend/app/routes/jira_add_connector_route.py b/surfsense_backend/app/routes/jira_add_connector_route.py index 4cb595058..e2eb20500 100644 --- a/surfsense_backend/app/routes/jira_add_connector_route.py +++ b/surfsense_backend/app/routes/jira_add_connector_route.py @@ -342,7 +342,7 @@ async def jira_callback( # Redirect to the frontend with success params return RedirectResponse( - url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&success=true&connector=jira-connector" + url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&success=true&connector=jira-connector&connectorId={new_connector.id}" ) except ValidationError as e: diff --git a/surfsense_backend/app/routes/linear_add_connector_route.py b/surfsense_backend/app/routes/linear_add_connector_route.py index 73bf500a3..f7a200322 100644 --- a/surfsense_backend/app/routes/linear_add_connector_route.py +++ b/surfsense_backend/app/routes/linear_add_connector_route.py @@ -293,7 +293,7 @@ async def linear_callback( # Redirect to the frontend with success params return RedirectResponse( - url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&success=true&connector=linear-connector" + url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&success=true&connector=linear-connector&connectorId={new_connector.id}" ) except ValidationError as e: diff --git a/surfsense_backend/app/routes/notion_add_connector_route.py b/surfsense_backend/app/routes/notion_add_connector_route.py index 251814d58..501c17e18 100644 --- a/surfsense_backend/app/routes/notion_add_connector_route.py +++ b/surfsense_backend/app/routes/notion_add_connector_route.py @@ -298,7 +298,7 @@ async def notion_callback( # Redirect to the frontend with success params return RedirectResponse( - url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&success=true&connector=notion-connector" + url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&success=true&connector=notion-connector&connectorId={new_connector.id}" ) except ValidationError as e: diff --git a/surfsense_backend/app/routes/slack_add_connector_route.py b/surfsense_backend/app/routes/slack_add_connector_route.py index 50c505a78..4917dae6d 100644 --- a/surfsense_backend/app/routes/slack_add_connector_route.py +++ b/surfsense_backend/app/routes/slack_add_connector_route.py @@ -309,7 +309,7 @@ async def slack_callback( # Redirect to the frontend with success params return RedirectResponse( - url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&success=true&connector=slack-connector" + url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&success=true&connector=slack-connector&connectorId={new_connector.id}" ) except ValidationError as e: diff --git a/surfsense_web/components/assistant-ui/connector-popup.tsx b/surfsense_web/components/assistant-ui/connector-popup.tsx index c5e996c4c..cb98d3731 100644 --- a/surfsense_web/components/assistant-ui/connector-popup.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup.tsx @@ -86,7 +86,6 @@ export const ConnectorIndicator: FC = () => { handleBackFromYouTube, handleViewAccountsList, handleBackFromAccountsList, - handleAddAccountOAuth, handleQuickIndexConnector, connectorConfig, setConnectorConfig, @@ -214,7 +213,7 @@ export const ConnectorIndicator: FC = () => { (c) => c.connectorType === viewingAccountsType.connectorType ); if (oauthConnector) { - handleAddAccountOAuth(oauthConnector); + handleConnectOAuth(oauthConnector); } }} isConnecting={connectingId !== null} diff --git a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/indexing-configuration-view.tsx b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/indexing-configuration-view.tsx index d479dda8d..2dcadf459 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/indexing-configuration-view.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/indexing-configuration-view.tsx @@ -8,8 +8,10 @@ import type { SearchSourceConnector } from "@/contracts/types/connector.types"; import { cn } from "@/lib/utils"; import { DateRangeSelector } from "../../components/date-range-selector"; import { PeriodicSyncConfig } from "../../components/periodic-sync-config"; -import type { IndexingConfigState } from "../../constants/connector-constants"; +import { OAUTH_CONNECTORS, type IndexingConfigState } from "../../constants/connector-constants"; import { getConnectorConfigComponent } from "../index"; +import { getConnectorTypeDisplay } from "@/lib/connectors/utils"; +import { getConnectorDisplayName } from "../../tabs/all-connectors-tab"; interface IndexingConfigurationViewProps { config: IndexingConfigState; @@ -89,12 +91,14 @@ export const IndexingConfigurationView: FC = ({ }; }, [checkScrollState]); + const authConnector = OAUTH_CONNECTORS.find((c) => c.connectorType === connector?.connector_type); + return (
    {/* Fixed Header */}
    @@ -111,14 +115,14 @@ export const IndexingConfigurationView: FC = ({ )} {/* Success header */} -
    +
    -

    - {config.connectorTitle} Connected! -

    +
    + {getConnectorTypeDisplay(connector?.connector_type || "")} Connected ! {getConnectorDisplayName(connector?.name || "")} +

    Configure when to start syncing your data

    diff --git a/surfsense_web/components/assistant-ui/connector-popup/hooks/use-connector-dialog.ts b/surfsense_web/components/assistant-ui/connector-popup/hooks/use-connector-dialog.ts index a9d4871e1..53774b76d 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/hooks/use-connector-dialog.ts +++ b/surfsense_web/components/assistant-ui/connector-popup/hooks/use-connector-dialog.ts @@ -148,14 +148,22 @@ export const useConnectorDialog = () => { // YouTube view is active - no additional state needed } - if (params.view === "configure" && params.connector && !indexingConfig) { + // Handle configure view (for page refresh support) + if (params.view === "configure" && params.connector && !indexingConfig && allConnectors) { const oauthConnector = OAUTH_CONNECTORS.find((c) => c.id === params.connector); - if (oauthConnector && allConnectors) { - const existingConnector = allConnectors.find( - (c: SearchSourceConnector) => c.connector_type === oauthConnector.connectorType - ); + if (oauthConnector) { + let existingConnector: SearchSourceConnector | undefined; + if (params.connectorId) { + const connectorId = parseInt(params.connectorId, 10); + existingConnector = allConnectors.find( + (c: SearchSourceConnector) => c.id === connectorId + ); + } else { + existingConnector = allConnectors.find( + (c: SearchSourceConnector) => c.connector_type === oauthConnector.connectorType + ); + } if (existingConnector) { - // Validate connector data before setting state const connectorValidation = searchSourceConnector.safeParse(existingConnector); if (connectorValidation.success) { const config = validateIndexingConfigState({ @@ -253,11 +261,19 @@ export const useConnectorDialog = () => { refetchAllConnectors().then((result) => { if (!result.data) return; - const newConnector = result.data.find( - (c: SearchSourceConnector) => c.connector_type === oauthConnector.connectorType - ); + let newConnector: SearchSourceConnector | undefined; + if (params.connectorId) { + const connectorId = parseInt(params.connectorId, 10); + newConnector = result.data.find( + (c: SearchSourceConnector) => c.id === connectorId + ); + } else { + newConnector = result.data.find( + (c: SearchSourceConnector) => c.connector_type === oauthConnector.connectorType + ); + } + if (newConnector) { - // Validate connector data before setting state const connectorValidation = searchSourceConnector.safeParse(newConnector); if (connectorValidation.success) { const config = validateIndexingConfigState({ @@ -271,6 +287,7 @@ export const useConnectorDialog = () => { setIsOpen(true); const url = new URL(window.location.href); url.searchParams.delete("success"); + url.searchParams.set("connectorId", newConnector.id.toString()); url.searchParams.set("view", "configure"); window.history.replaceState({}, "", url.toString()); } else { @@ -691,39 +708,6 @@ export const useConnectorDialog = () => { router.replace(url.pathname + url.search, { scroll: false }); }, [router]); - // Handle adding a new account for OAuth connector (from accounts list view) - const handleAddAccountOAuth = useCallback( - async (connector: (typeof OAUTH_CONNECTORS)[number]) => { - if (!searchSpaceId || !connector.authEndpoint) return; - - // Set connecting state - setConnectingId(connector.id); - - try { - const response = await authenticatedFetch( - `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}${connector.authEndpoint}?space_id=${searchSpaceId}`, - { method: "GET" } - ); - - if (!response.ok) { - throw new Error(`Failed to initiate ${connector.title} OAuth`); - } - - const data = await response.json(); - const validatedData = parseOAuthAuthResponse(data); - window.location.href = validatedData.auth_url; - } catch (error) { - console.error(`Error connecting to ${connector.title}:`, error); - if (error instanceof Error && error.message.includes("Invalid auth URL")) { - toast.error(`Invalid response from ${connector.title} OAuth endpoint`); - } else { - toast.error(`Failed to connect to ${connector.title}`); - } - setConnectingId(null); - } - }, - [searchSpaceId] - ); // Handle starting indexing const handleStartIndexing = useCallback( @@ -1249,7 +1233,6 @@ export const useConnectorDialog = () => { handleBackFromYouTube, handleViewAccountsList, handleBackFromAccountsList, - handleAddAccountOAuth, handleQuickIndexConnector, connectorConfig, setConnectorConfig, From 3ff87a218dbf3df40503190d3980eafb800ca39f Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Wed, 7 Jan 2026 11:40:21 +0200 Subject: [PATCH 58/75] feat: improve connector popup with grouped OAuth connectors Active Connectors tab: - Group OAuth connectors by type (Gmail, Google Drive, etc.) - Show account count badge on grouped cards - Show most recent last indexed date across all accounts - Show non-OAuth connectors individually with active task messages All Connectors tab: - Show most recent last indexed date for OAuth connector types - Check if any account is indexing for OAuth types Accounts List View: - Remove document count from individual account cards - Back button returns to previous tab (not always All Connectors) General: - Update handleViewAccountsList to use (connectorType, connectorTitle) signature - Consistent behavior for viewing accounts from both tabs --- .../assistant-ui/connector-popup.tsx | 26 +-- .../hooks/use-connector-dialog.ts | 13 +- .../tabs/active-connectors-tab.tsx | 181 ++++++++++++++---- .../tabs/all-connectors-tab.tsx | 41 ++-- .../views/connector-accounts-list-view.tsx | 24 --- surfsense_web/lib/connectors/utils.ts | 1 + 6 files changed, 193 insertions(+), 93 deletions(-) diff --git a/surfsense_web/components/assistant-ui/connector-popup.tsx b/surfsense_web/components/assistant-ui/connector-popup.tsx index cb98d3731..0f8d341c2 100644 --- a/surfsense_web/components/assistant-ui/connector-popup.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup.tsx @@ -205,7 +205,6 @@ export const ConnectorIndicator: FC = () => { connectors={(allConnectors || []) as SearchSourceConnector[]} indexingConnectorIds={indexingConnectorIds} logsSummary={logsSummary} - documentTypeCounts={documentTypeCounts} onBack={handleBackFromAccountsList} onManage={handleStartEdit} onAddAccount={() => { @@ -317,18 +316,19 @@ export const ConnectorIndicator: FC = () => { /> - +
    {/* Bottom fade shadow */} diff --git a/surfsense_web/components/assistant-ui/connector-popup/hooks/use-connector-dialog.ts b/surfsense_web/components/assistant-ui/connector-popup/hooks/use-connector-dialog.ts index 53774b76d..3ab65dd89 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/hooks/use-connector-dialog.ts +++ b/surfsense_web/components/assistant-ui/connector-popup/hooks/use-connector-dialog.ts @@ -679,19 +679,20 @@ export const useConnectorDialog = () => { // Handle viewing accounts list for OAuth connector type const handleViewAccountsList = useCallback( - (connector: (typeof OAUTH_CONNECTORS)[number]) => { + (connectorType: string, connectorTitle: string) => { if (!searchSpaceId) return; setViewingAccountsType({ - connectorType: connector.connectorType, - connectorTitle: connector.title, + connectorType, + connectorTitle, }); - // Update URL to show accounts view + // Update URL to show accounts view, preserving current tab const url = new URL(window.location.href); url.searchParams.set("modal", "connectors"); url.searchParams.set("view", "accounts"); - url.searchParams.set("connectorType", connector.connectorType); + url.searchParams.set("connectorType", connectorType); + // Keep the current tab in URL so we can go back to it window.history.pushState({ modal: true }, "", url.toString()); }, [searchSpaceId] @@ -702,7 +703,7 @@ export const useConnectorDialog = () => { setViewingAccountsType(null); const url = new URL(window.location.href); url.searchParams.set("modal", "connectors"); - url.searchParams.set("tab", "all"); + // Keep the current tab (don't change it) - just remove view-specific params url.searchParams.delete("view"); url.searchParams.delete("connectorType"); router.replace(url.pathname + url.search, { scroll: false }); diff --git a/surfsense_web/components/assistant-ui/connector-popup/tabs/active-connectors-tab.tsx b/surfsense_web/components/assistant-ui/connector-popup/tabs/active-connectors-tab.tsx index 04e819bc8..d2f8a7fa6 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/tabs/active-connectors-tab.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/tabs/active-connectors-tab.tsx @@ -11,8 +11,8 @@ import { getConnectorIcon } from "@/contracts/enums/connectorIcons"; import type { SearchSourceConnector } from "@/contracts/types/connector.types"; import type { LogActiveTask, LogSummary } from "@/contracts/types/log.types"; import { cn } from "@/lib/utils"; +import { OAUTH_CONNECTORS } from "../constants/connector-constants"; import { getDocumentCountForConnector } from "../utils/connector-document-mapping"; -import { getConnectorDisplayName } from "./all-connectors-tab"; interface ActiveConnectorsTabProps { searchQuery: string; @@ -25,6 +25,7 @@ interface ActiveConnectorsTabProps { searchSpaceId: string; onTabChange: (value: string) => void; onManage?: (connector: SearchSourceConnector) => void; + onViewAccountsList?: (connectorType: string, connectorTitle: string) => void; } export const ActiveConnectorsTab: FC = ({ @@ -37,6 +38,7 @@ export const ActiveConnectorsTab: FC = ({ searchSpaceId, onTabChange, onManage, + onViewAccountsList, }) => { const router = useRouter(); @@ -72,38 +74,24 @@ export const ActiveConnectorsTab: FC = ({ const minutesAgo = differenceInMinutes(now, date); const daysAgo = differenceInDays(now, date); - // Just now (within last minute) - if (minutesAgo < 1) { - return "Just now"; - } - - // X minutes ago (less than 1 hour) - if (minutesAgo < 60) { - return `${minutesAgo} ${minutesAgo === 1 ? "minute" : "minutes"} ago`; - } - - // Today at [time] - if (isToday(date)) { - return `Today at ${format(date, "h:mm a")}`; - } - - // Yesterday at [time] - if (isYesterday(date)) { - return `Yesterday at ${format(date, "h:mm a")}`; - } - - // X days ago (less than 7 days) - if (daysAgo < 7) { - return `${daysAgo} ${daysAgo === 1 ? "day" : "days"} ago`; - } - - // Full date for older entries + if (minutesAgo < 1) return "Just now"; + if (minutesAgo < 60) return `${minutesAgo} ${minutesAgo === 1 ? "minute" : "minutes"} ago`; + if (isToday(date)) return `Today at ${format(date, "h:mm a")}`; + if (isYesterday(date)) return `Yesterday at ${format(date, "h:mm a")}`; + if (daysAgo < 7) return `${daysAgo} ${daysAgo === 1 ? "day" : "days"} ago`; return format(date, "MMM d, yyyy"); }; - // Document types that should be shown as cards (not from connectors) - // These are: EXTENSION (browser extension), FILE (uploaded files), NOTE (editor notes), - // YOUTUBE_VIDEO (YouTube videos), and CRAWLED_URL (web pages - shown separately even though it can come from WEBCRAWLER_CONNECTOR) + // Get most recent last indexed date from a list of connectors + const getMostRecentLastIndexed = (connectorsList: SearchSourceConnector[]): string | undefined => { + return connectorsList.reduce((latest, c) => { + if (!c.last_indexed_at) return latest; + if (!latest) return c.last_indexed_at; + return new Date(c.last_indexed_at) > new Date(latest) ? c.last_indexed_at : latest; + }, undefined); + }; + + // Document types that should be shown as standalone cards (not from connectors) const standaloneDocumentTypes = ["EXTENSION", "FILE", "NOTE", "YOUTUBE_VIDEO", "CRAWLED_URL"]; // Filter to only show standalone document types that have documents (count > 0) @@ -119,8 +107,47 @@ export const ActiveConnectorsTab: FC = ({ return doc.label.toLowerCase().includes(searchQuery.toLowerCase()); }); - // Filter connectors based on search query - const filteredConnectors = connectors.filter((connector) => { + // Get OAuth connector types set for quick lookup + const oauthConnectorTypes = new Set(OAUTH_CONNECTORS.map((c) => c.connectorType)); + + // Separate OAuth and non-OAuth connectors + const oauthConnectors = connectors.filter((c) => oauthConnectorTypes.has(c.connector_type)); + const nonOauthConnectors = connectors.filter((c) => !oauthConnectorTypes.has(c.connector_type)); + + // Group OAuth connectors by type + const oauthConnectorsByType = oauthConnectors.reduce( + (acc, connector) => { + const type = connector.connector_type; + if (!acc[type]) { + acc[type] = []; + } + acc[type].push(connector); + return acc; + }, + {} as Record + ); + + // Get display info for OAuth connector type + const getOAuthConnectorTypeInfo = (connectorType: string) => { + const oauthConnector = OAUTH_CONNECTORS.find((c) => c.connectorType === connectorType); + return { + title: oauthConnector?.title || connectorType.replace(/_/g, " ").replace(/connector/gi, "").trim(), + }; + }; + + // Filter OAuth connector types based on search query + const filteredOAuthConnectorTypes = Object.entries(oauthConnectorsByType).filter(([connectorType]) => { + if (!searchQuery) return true; + const searchLower = searchQuery.toLowerCase(); + const { title } = getOAuthConnectorTypeInfo(connectorType); + return ( + title.toLowerCase().includes(searchLower) || + connectorType.toLowerCase().includes(searchLower) + ); + }); + + // Filter non-OAuth connectors based on search query + const filteredNonOAuthConnectors = nonOauthConnectors.filter((connector) => { if (!searchQuery) return true; const searchLower = searchQuery.toLowerCase(); return ( @@ -129,18 +156,98 @@ export const ActiveConnectorsTab: FC = ({ ); }); + const hasActiveConnectors = filteredOAuthConnectorTypes.length > 0 || filteredNonOAuthConnectors.length > 0; + return ( {hasSources ? (
    {/* Active Connectors Section */} - {filteredConnectors.length > 0 && ( + {hasActiveConnectors && (

    Active Connectors

    - {filteredConnectors.map((connector) => { + {/* OAuth Connectors - Grouped by Type */} + {filteredOAuthConnectorTypes.map(([connectorType, typeConnectors]) => { + const { title } = getOAuthConnectorTypeInfo(connectorType); + const isAnyIndexing = typeConnectors.some( + (c: SearchSourceConnector) => indexingConnectorIds.has(c.id) + ); + const documentCount = getDocumentCountForConnector( + connectorType, + documentTypeCounts + ); + const accountCount = typeConnectors.length; + const mostRecentLastIndexed = getMostRecentLastIndexed(typeConnectors); + + const handleManageClick = () => { + if (onViewAccountsList) { + onViewAccountsList(connectorType, title); + } else if (onManage && typeConnectors[0]) { + onManage(typeConnectors[0]); + } + }; + + return ( +
    + {/* Account count badge */} +
    + {accountCount > 99 ? "99+" : accountCount} {accountCount === 1 ? "Account" : "Accounts"} +
    +
    + {getConnectorIcon(connectorType, "size-6")} +
    +
    +

    + {title} +

    + {isAnyIndexing ? ( +

    + + Indexing... +

    + ) : ( +

    + {mostRecentLastIndexed + ? `Last indexed: ${formatLastIndexedDate(mostRecentLastIndexed)}` + : "Never indexed"} +

    + )} +

    + {formatDocumentCount(documentCount)} +

    +
    + +
    + ); + })} + + {/* Non-OAuth Connectors - Individual Cards */} + {filteredNonOAuthConnectors.map((connector) => { const isIndexing = indexingConnectorIds.has(connector.id); const activeTask = logsSummary?.active_tasks?.find( (task: LogActiveTask) => task.connector_id === connector.id @@ -162,7 +269,7 @@ export const ActiveConnectorsTab: FC = ({ >
    = ({

    - {getConnectorDisplayName(connector.name)} + {connector.name}

    {isIndexing ? (

    @@ -198,7 +305,7 @@ export const ActiveConnectorsTab: FC = ({

    diff --git a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-connect-view.tsx b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-connect-view.tsx index 22dff4322..e3941367b 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-connect-view.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-connect-view.tsx @@ -54,7 +54,6 @@ export const ConnectorConnectView: FC = ({ ELASTICSEARCH_CONNECTOR: "elasticsearch-connect-form", BOOKSTACK_CONNECTOR: "bookstack-connect-form", GITHUB_CONNECTOR: "github-connect-form", - CLICKUP_CONNECTOR: "clickup-connect-form", LUMA_CONNECTOR: "luma-connect-form", CIRCLEBACK_CONNECTOR: "circleback-connect-form", }; diff --git a/surfsense_web/components/assistant-ui/connector-popup/constants/connector-constants.ts b/surfsense_web/components/assistant-ui/connector-popup/constants/connector-constants.ts index 4d15d0989..287bc30f4 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/constants/connector-constants.ts +++ b/surfsense_web/components/assistant-ui/connector-popup/constants/connector-constants.ts @@ -72,6 +72,13 @@ export const OAUTH_CONNECTORS = [ connectorType: EnumConnectorName.CONFLUENCE_CONNECTOR, authEndpoint: "/api/v1/auth/confluence/connector/add/", }, + { + id: "clickup-connector", + title: "ClickUp", + description: "Search ClickUp tasks", + connectorType: EnumConnectorName.CLICKUP_CONNECTOR, + authEndpoint: "/api/v1/auth/clickup/connector/add/", + }, ] as const; // Content Sources (tools that extract and import content from external sources) @@ -104,12 +111,6 @@ export const OTHER_CONNECTORS = [ description: "Search repositories", connectorType: EnumConnectorName.GITHUB_CONNECTOR, }, - { - id: "clickup-connector", - title: "ClickUp", - description: "Search ClickUp tasks", - connectorType: EnumConnectorName.CLICKUP_CONNECTOR, - }, { id: "luma-connector", title: "Luma", From ca46822d6d2a2d453332178efb1db45a83a7ce21 Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Wed, 7 Jan 2026 15:16:09 +0530 Subject: [PATCH 60/75] chore: ran frontend linting --- .../connector-configs/components/clickup-config.tsx | 7 ++++--- surfsense_web/content/docs/connectors/meta.json | 1 - 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/clickup-config.tsx b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/clickup-config.tsx index 940ec912b..5b7ddaeb8 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/clickup-config.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/clickup-config.tsx @@ -63,7 +63,8 @@ export const ClickUpConfig: FC = ({

    Connected via OAuth

    - Workspace: {workspaceName} + Workspace:{" "} + {workspaceName}

    To update your connection, reconnect this connector. @@ -112,8 +113,8 @@ export const ClickUpConfig: FC = ({ className="border-slate-400/20 focus-visible:border-slate-400/40" />

    - Update your ClickUp API Token if needed. For better security and automatic token refresh, - consider disconnecting and reconnecting using OAuth 2.0. + Update your ClickUp API Token if needed. For better security and automatic token + refresh, consider disconnecting and reconnecting using OAuth 2.0.

    diff --git a/surfsense_web/content/docs/connectors/meta.json b/surfsense_web/content/docs/connectors/meta.json index 2515bc7d8..82e04e44f 100644 --- a/surfsense_web/content/docs/connectors/meta.json +++ b/surfsense_web/content/docs/connectors/meta.json @@ -20,4 +20,3 @@ ], "defaultOpen": true } - From 4b3d427e90fb936b5cf6e570213168b4d9240f02 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Wed, 7 Jan 2026 12:57:33 +0200 Subject: [PATCH 61/75] feat: prevent duplicate OAuth account connections --- .../routes/airtable_add_connector_route.py | 18 +++++++++- .../routes/confluence_add_connector_route.py | 18 ++++++++++ .../app/routes/discord_add_connector_route.py | 18 ++++++++++ .../google_calendar_add_connector_route.py | 18 +++++++++- .../google_drive_add_connector_route.py | 18 +++++++++- .../google_gmail_add_connector_route.py | 18 +++++++++- .../app/routes/jira_add_connector_route.py | 18 ++++++++++ .../app/routes/linear_add_connector_route.py | 18 +++++++++- .../app/routes/notion_add_connector_route.py | 18 ++++++++++ .../app/routes/slack_add_connector_route.py | 18 ++++++++++ .../app/utils/connector_naming.py | 35 +++++++++++++++++++ .../constants/connector-popup.schemas.ts | 1 + .../hooks/use-connector-dialog.ts | 30 +++++++++++++++- 13 files changed, 240 insertions(+), 6 deletions(-) diff --git a/surfsense_backend/app/routes/airtable_add_connector_route.py b/surfsense_backend/app/routes/airtable_add_connector_route.py index 93a263ed0..92fcbc67e 100644 --- a/surfsense_backend/app/routes/airtable_add_connector_route.py +++ b/surfsense_backend/app/routes/airtable_add_connector_route.py @@ -23,7 +23,7 @@ from app.db import ( from app.connectors.airtable_connector import fetch_airtable_user_email from app.schemas.airtable_auth_credentials import AirtableAuthCredentialsBase from app.users import current_active_user -from app.utils.connector_naming import generate_unique_connector_name +from app.utils.connector_naming import check_duplicate_connector, generate_unique_connector_name from app.utils.oauth_security import OAuthStateManager, TokenEncryption logger = logging.getLogger(__name__) @@ -303,6 +303,22 @@ async def airtable_callback( credentials_dict = credentials.to_dict() credentials_dict["_token_encrypted"] = True + # Check for duplicate connector (same account already connected) + is_duplicate = await check_duplicate_connector( + session, + SearchSourceConnectorType.AIRTABLE_CONNECTOR, + space_id, + user_id, + user_email, + ) + if is_duplicate: + logger.warning( + f"Duplicate Airtable connector detected for user {user_id} with email {user_email}" + ) + return RedirectResponse( + url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&error=duplicate_account&connector=airtable-connector" + ) + # Generate a unique, user-friendly connector name connector_name = await generate_unique_connector_name( session, diff --git a/surfsense_backend/app/routes/confluence_add_connector_route.py b/surfsense_backend/app/routes/confluence_add_connector_route.py index 284b4768a..56abf62ce 100644 --- a/surfsense_backend/app/routes/confluence_add_connector_route.py +++ b/surfsense_backend/app/routes/confluence_add_connector_route.py @@ -27,6 +27,7 @@ from app.schemas.atlassian_auth_credentials import AtlassianAuthCredentialsBase from app.users import current_active_user from app.utils.oauth_security import OAuthStateManager, TokenEncryption from app.utils.connector_naming import ( + check_duplicate_connector, extract_identifier_from_credentials, generate_unique_connector_name, ) @@ -296,6 +297,23 @@ async def confluence_callback( connector_identifier = extract_identifier_from_credentials( SearchSourceConnectorType.CONFLUENCE_CONNECTOR, connector_config ) + + # Check for duplicate connector (same Confluence instance already connected) + is_duplicate = await check_duplicate_connector( + session, + SearchSourceConnectorType.CONFLUENCE_CONNECTOR, + space_id, + user_id, + connector_identifier, + ) + if is_duplicate: + logger.warning( + f"Duplicate Confluence connector detected for user {user_id} with instance {connector_identifier}" + ) + return RedirectResponse( + url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&error=duplicate_account&connector=confluence-connector" + ) + # Generate a unique, user-friendly connector name connector_name = await generate_unique_connector_name( session, diff --git a/surfsense_backend/app/routes/discord_add_connector_route.py b/surfsense_backend/app/routes/discord_add_connector_route.py index 0bd864b89..0bda191c6 100644 --- a/surfsense_backend/app/routes/discord_add_connector_route.py +++ b/surfsense_backend/app/routes/discord_add_connector_route.py @@ -27,6 +27,7 @@ from app.schemas.discord_auth_credentials import DiscordAuthCredentialsBase from app.users import current_active_user from app.utils.oauth_security import OAuthStateManager, TokenEncryption from app.utils.connector_naming import ( + check_duplicate_connector, extract_identifier_from_credentials, generate_unique_connector_name, ) @@ -292,6 +293,23 @@ async def discord_callback( connector_identifier = extract_identifier_from_credentials( SearchSourceConnectorType.DISCORD_CONNECTOR, connector_config ) + + # Check for duplicate connector (same server already connected) + is_duplicate = await check_duplicate_connector( + session, + SearchSourceConnectorType.DISCORD_CONNECTOR, + space_id, + user_id, + connector_identifier, + ) + if is_duplicate: + logger.warning( + f"Duplicate Discord connector detected for user {user_id} with server {connector_identifier}" + ) + return RedirectResponse( + url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&error=duplicate_account&connector=discord-connector" + ) + # Generate a unique, user-friendly connector name connector_name = await generate_unique_connector_name( session, 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 0770ec030..a721b62c1 100644 --- a/surfsense_backend/app/routes/google_calendar_add_connector_route.py +++ b/surfsense_backend/app/routes/google_calendar_add_connector_route.py @@ -23,7 +23,7 @@ from app.db import ( get_async_session, ) from app.users import current_active_user -from app.utils.connector_naming import generate_unique_connector_name +from app.utils.connector_naming import check_duplicate_connector, generate_unique_connector_name from app.utils.oauth_security import OAuthStateManager, TokenEncryption logger = logging.getLogger(__name__) @@ -195,6 +195,22 @@ async def calendar_callback( # Mark that credentials are encrypted for backward compatibility creds_dict["_token_encrypted"] = True + # Check for duplicate connector (same account already connected) + is_duplicate = await check_duplicate_connector( + session, + SearchSourceConnectorType.GOOGLE_CALENDAR_CONNECTOR, + space_id, + user_id, + user_email, + ) + if is_duplicate: + logger.warning( + f"Duplicate Google Calendar connector detected for user {user_id} with email {user_email}" + ) + return RedirectResponse( + url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&error=duplicate_account&connector=google-calendar-connector" + ) + try: # Generate a unique, user-friendly connector name connector_name = await generate_unique_connector_name( diff --git a/surfsense_backend/app/routes/google_drive_add_connector_route.py b/surfsense_backend/app/routes/google_drive_add_connector_route.py index ba45d7a2f..1b02543d3 100644 --- a/surfsense_backend/app/routes/google_drive_add_connector_route.py +++ b/surfsense_backend/app/routes/google_drive_add_connector_route.py @@ -37,7 +37,7 @@ from app.db import ( get_async_session, ) from app.users import current_active_user -from app.utils.connector_naming import generate_unique_connector_name +from app.utils.connector_naming import check_duplicate_connector, generate_unique_connector_name from app.utils.oauth_security import OAuthStateManager, TokenEncryption # Relax token scope validation for Google OAuth @@ -250,6 +250,22 @@ async def drive_callback( # Mark that credentials are encrypted for backward compatibility creds_dict["_token_encrypted"] = True + # Check for duplicate connector (same account already connected) + is_duplicate = await check_duplicate_connector( + session, + SearchSourceConnectorType.GOOGLE_DRIVE_CONNECTOR, + space_id, + user_id, + user_email, + ) + if is_duplicate: + logger.warning( + f"Duplicate Google Drive connector detected for user {user_id} with email {user_email}" + ) + return RedirectResponse( + url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&error=duplicate_account&connector=google-drive-connector" + ) + # Generate a unique, user-friendly connector name connector_name = await generate_unique_connector_name( session, diff --git a/surfsense_backend/app/routes/google_gmail_add_connector_route.py b/surfsense_backend/app/routes/google_gmail_add_connector_route.py index 6baeca83c..4a7631919 100644 --- a/surfsense_backend/app/routes/google_gmail_add_connector_route.py +++ b/surfsense_backend/app/routes/google_gmail_add_connector_route.py @@ -23,7 +23,7 @@ from app.db import ( get_async_session, ) from app.users import current_active_user -from app.utils.connector_naming import generate_unique_connector_name +from app.utils.connector_naming import check_duplicate_connector, generate_unique_connector_name from app.utils.oauth_security import OAuthStateManager, TokenEncryption logger = logging.getLogger(__name__) @@ -226,6 +226,22 @@ async def gmail_callback( # Mark that credentials are encrypted for backward compatibility creds_dict["_token_encrypted"] = True + # Check for duplicate connector (same account already connected) + is_duplicate = await check_duplicate_connector( + session, + SearchSourceConnectorType.GOOGLE_GMAIL_CONNECTOR, + space_id, + user_id, + user_email, + ) + if is_duplicate: + logger.warning( + f"Duplicate Gmail connector detected for user {user_id} with email {user_email}" + ) + return RedirectResponse( + url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&error=duplicate_account&connector=google-gmail-connector" + ) + try: # Generate a unique, user-friendly connector name connector_name = await generate_unique_connector_name( diff --git a/surfsense_backend/app/routes/jira_add_connector_route.py b/surfsense_backend/app/routes/jira_add_connector_route.py index e2eb20500..744cf7fd4 100644 --- a/surfsense_backend/app/routes/jira_add_connector_route.py +++ b/surfsense_backend/app/routes/jira_add_connector_route.py @@ -28,6 +28,7 @@ from app.schemas.atlassian_auth_credentials import AtlassianAuthCredentialsBase from app.users import current_active_user from app.utils.oauth_security import OAuthStateManager, TokenEncryption from app.utils.connector_naming import ( + check_duplicate_connector, extract_identifier_from_credentials, generate_unique_connector_name, ) @@ -314,6 +315,23 @@ async def jira_callback( connector_identifier = extract_identifier_from_credentials( SearchSourceConnectorType.JIRA_CONNECTOR, connector_config ) + + # Check for duplicate connector (same Jira instance already connected) + is_duplicate = await check_duplicate_connector( + session, + SearchSourceConnectorType.JIRA_CONNECTOR, + space_id, + user_id, + connector_identifier, + ) + if is_duplicate: + logger.warning( + f"Duplicate Jira connector detected for user {user_id} with instance {connector_identifier}" + ) + return RedirectResponse( + url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&error=duplicate_account&connector=jira-connector" + ) + # Generate a unique, user-friendly connector name connector_name = await generate_unique_connector_name( session, diff --git a/surfsense_backend/app/routes/linear_add_connector_route.py b/surfsense_backend/app/routes/linear_add_connector_route.py index f7a200322..ca1d09568 100644 --- a/surfsense_backend/app/routes/linear_add_connector_route.py +++ b/surfsense_backend/app/routes/linear_add_connector_route.py @@ -26,7 +26,7 @@ from app.db import ( from app.connectors.linear_oauth import fetch_linear_organization_name from app.schemas.linear_auth_credentials import LinearAuthCredentialsBase from app.users import current_active_user -from app.utils.connector_naming import generate_unique_connector_name +from app.utils.connector_naming import check_duplicate_connector, generate_unique_connector_name from app.utils.oauth_security import OAuthStateManager, TokenEncryption logger = logging.getLogger(__name__) @@ -265,6 +265,22 @@ async def linear_callback( "_token_encrypted": True, } + # Check for duplicate connector (same organization already connected) + is_duplicate = await check_duplicate_connector( + session, + SearchSourceConnectorType.LINEAR_CONNECTOR, + space_id, + user_id, + org_name, + ) + if is_duplicate: + logger.warning( + f"Duplicate Linear connector detected for user {user_id} with org {org_name}" + ) + return RedirectResponse( + url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&error=duplicate_account&connector=linear-connector" + ) + # Generate a unique, user-friendly connector name connector_name = await generate_unique_connector_name( session, diff --git a/surfsense_backend/app/routes/notion_add_connector_route.py b/surfsense_backend/app/routes/notion_add_connector_route.py index 501c17e18..c0331b4bc 100644 --- a/surfsense_backend/app/routes/notion_add_connector_route.py +++ b/surfsense_backend/app/routes/notion_add_connector_route.py @@ -27,6 +27,7 @@ from app.schemas.notion_auth_credentials import NotionAuthCredentialsBase from app.users import current_active_user from app.utils.oauth_security import OAuthStateManager, TokenEncryption from app.utils.connector_naming import ( + check_duplicate_connector, extract_identifier_from_credentials, generate_unique_connector_name, ) @@ -270,6 +271,23 @@ async def notion_callback( connector_identifier = extract_identifier_from_credentials( SearchSourceConnectorType.NOTION_CONNECTOR, connector_config ) + + # Check for duplicate connector (same workspace already connected) + is_duplicate = await check_duplicate_connector( + session, + SearchSourceConnectorType.NOTION_CONNECTOR, + space_id, + user_id, + connector_identifier, + ) + if is_duplicate: + logger.warning( + f"Duplicate Notion connector detected for user {user_id} with workspace {connector_identifier}" + ) + return RedirectResponse( + url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&error=duplicate_account&connector=notion-connector" + ) + # Generate a unique, user-friendly connector name connector_name = await generate_unique_connector_name( session, diff --git a/surfsense_backend/app/routes/slack_add_connector_route.py b/surfsense_backend/app/routes/slack_add_connector_route.py index 4917dae6d..6da7e5d24 100644 --- a/surfsense_backend/app/routes/slack_add_connector_route.py +++ b/surfsense_backend/app/routes/slack_add_connector_route.py @@ -26,6 +26,7 @@ from app.db import ( from app.schemas.slack_auth_credentials import SlackAuthCredentialsBase from app.users import current_active_user from app.utils.connector_naming import ( + check_duplicate_connector, extract_identifier_from_credentials, generate_unique_connector_name, ) @@ -280,6 +281,23 @@ async def slack_callback( connector_identifier = extract_identifier_from_credentials( SearchSourceConnectorType.SLACK_CONNECTOR, connector_config ) + + # Check for duplicate connector (same workspace already connected) + is_duplicate = await check_duplicate_connector( + session, + SearchSourceConnectorType.SLACK_CONNECTOR, + space_id, + user_id, + connector_identifier, + ) + if is_duplicate: + logger.warning( + f"Duplicate Slack connector detected for user {user_id} with workspace {connector_identifier}" + ) + return RedirectResponse( + url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&error=duplicate_account&connector=slack-connector" + ) + # Generate a unique, user-friendly connector name connector_name = await generate_unique_connector_name( session, diff --git a/surfsense_backend/app/utils/connector_naming.py b/surfsense_backend/app/utils/connector_naming.py index 6f582cd87..17b791c34 100644 --- a/surfsense_backend/app/utils/connector_naming.py +++ b/surfsense_backend/app/utils/connector_naming.py @@ -114,6 +114,41 @@ async def count_connectors_of_type( return result.scalar() or 0 +async def check_duplicate_connector( + session: AsyncSession, + connector_type: SearchSourceConnectorType, + search_space_id: int, + user_id: UUID, + identifier: str | None, +) -> bool: + """ + Check if a connector with the same identifier already exists. + + Args: + session: Database session + connector_type: The type of connector + search_space_id: The search space ID + user_id: The user ID + identifier: User identifier (email, workspace name, etc.) + + Returns: + True if a duplicate exists, False otherwise + """ + if not identifier: + return False + + expected_name = f"{get_base_name_for_type(connector_type)} - {identifier}" + result = await session.execute( + select(func.count(SearchSourceConnector.id)).where( + SearchSourceConnector.connector_type == connector_type, + SearchSourceConnector.search_space_id == search_space_id, + SearchSourceConnector.user_id == user_id, + SearchSourceConnector.name == expected_name, + ) + ) + return (result.scalar() or 0) > 0 + + async def generate_unique_connector_name( session: AsyncSession, connector_type: SearchSourceConnectorType, diff --git a/surfsense_web/components/assistant-ui/connector-popup/constants/connector-popup.schemas.ts b/surfsense_web/components/assistant-ui/connector-popup/constants/connector-popup.schemas.ts index 808c7b428..a1b303163 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/constants/connector-popup.schemas.ts +++ b/surfsense_web/components/assistant-ui/connector-popup/constants/connector-popup.schemas.ts @@ -12,6 +12,7 @@ export const connectorPopupQueryParamsSchema = z.object({ connectorId: z.string().optional(), connectorType: z.string().optional(), success: z.enum(["true", "false"]).optional(), + error: z.string().optional(), }); export type ConnectorPopupQueryParams = z.infer; diff --git a/surfsense_web/components/assistant-ui/connector-popup/hooks/use-connector-dialog.ts b/surfsense_web/components/assistant-ui/connector-popup/hooks/use-connector-dialog.ts index 3ab65dd89..1bfef9c43 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/hooks/use-connector-dialog.ts +++ b/surfsense_web/components/assistant-ui/connector-popup/hooks/use-connector-dialog.ts @@ -245,11 +245,39 @@ export const useConnectorDialog = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [searchParams, allConnectors, editingConnector, indexingConfig, connectingConnectorType, viewingAccountsType]); - // Detect OAuth success and transition to config view + // Detect OAuth success / Failure and transition to config view useEffect(() => { try { const params = parseConnectorPopupQueryParams(searchParams); + // Handle OAuth errors (e.g., duplicate account) + if (params.error && params.modal === "connectors") { + const oauthConnector = params.connector + ? OAUTH_CONNECTORS.find((c) => c.id === params.connector) + : null; + const connectorName = oauthConnector?.title || "connector"; + + if (params.error === "duplicate_account") { + toast.error(`This ${connectorName} account is already connected`, { + description: "Please use a different account or manage the existing connection.", + }); + } else { + toast.error(`Failed to connect ${connectorName}`, { + description: params.error.replace(/_/g, " "), + }); + } + + // Clean up error params from URL + const url = new URL(window.location.href); + url.searchParams.delete("error"); + url.searchParams.delete("connector"); + window.history.replaceState({}, "", url.toString()); + + // Open the popup to show the connectors + setIsOpen(true); + return; + } + if ( params.success === "true" && params.connector && From f1a715e04e37fd793fa95abb1dfcbc3bc553a6e9 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Wed, 7 Jan 2026 13:13:19 +0200 Subject: [PATCH 62/75] refactor: move Linear OAuth utils to connector, use httpx.AsyncClient --- .../app/connectors/airtable_connector.py | 5 -- .../app/connectors/linear_connector.py | 48 +++++++++++++++ .../app/connectors/linear_oauth.py | 60 ------------------- .../app/routes/linear_add_connector_route.py | 4 +- 4 files changed, 50 insertions(+), 67 deletions(-) delete mode 100644 surfsense_backend/app/connectors/linear_oauth.py diff --git a/surfsense_backend/app/connectors/airtable_connector.py b/surfsense_backend/app/connectors/airtable_connector.py index ecbba7a19..30a366cdd 100644 --- a/surfsense_backend/app/connectors/airtable_connector.py +++ b/surfsense_backend/app/connectors/airtable_connector.py @@ -399,11 +399,6 @@ async def fetch_airtable_user_email(access_token: str) -> str | None: Returns: User's email address or None if fetch fails """ - import httpx - import logging - - logger = logging.getLogger(__name__) - try: async with httpx.AsyncClient() as client: response = await client.get( diff --git a/surfsense_backend/app/connectors/linear_connector.py b/surfsense_backend/app/connectors/linear_connector.py index 404f60e66..61980e7ba 100644 --- a/surfsense_backend/app/connectors/linear_connector.py +++ b/surfsense_backend/app/connectors/linear_connector.py @@ -9,6 +9,7 @@ import logging from datetime import datetime from typing import Any +import httpx import requests from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.future import select @@ -21,6 +22,53 @@ from app.utils.oauth_security import TokenEncryption logger = logging.getLogger(__name__) +LINEAR_GRAPHQL_URL = "https://api.linear.app/graphql" + +ORGANIZATION_QUERY = """ +query { + organization { + name + } +} +""" + + +async def fetch_linear_organization_name(access_token: str) -> str | None: + """ + Fetch organization/workspace name from Linear GraphQL API. + + Args: + access_token: The Linear OAuth access token + + Returns: + Organization name or None if fetch fails + """ + try: + async with httpx.AsyncClient() as client: + response = await client.post( + LINEAR_GRAPHQL_URL, + headers={ + "Authorization": access_token, + "Content-Type": "application/json", + }, + json={"query": ORGANIZATION_QUERY}, + timeout=10.0, + ) + + if response.status_code == 200: + data = response.json() + org_name = data.get("data", {}).get("organization", {}).get("name") + if org_name: + logger.debug(f"Fetched Linear organization name: {org_name}") + return org_name + + logger.warning(f"Failed to fetch Linear org info: {response.status_code}") + return None + + except Exception as e: + logger.warning(f"Error fetching Linear organization name: {e!s}") + return None + class LinearConnector: """Class for retrieving issues and comments from Linear.""" diff --git a/surfsense_backend/app/connectors/linear_oauth.py b/surfsense_backend/app/connectors/linear_oauth.py deleted file mode 100644 index 96336fe94..000000000 --- a/surfsense_backend/app/connectors/linear_oauth.py +++ /dev/null @@ -1,60 +0,0 @@ -""" -Linear OAuth Utilities. - -Provides functions for fetching user/organization info from Linear API. -Separated from linear_connector.py to avoid circular imports. -""" - -import logging - -import httpx - -logger = logging.getLogger(__name__) - -LINEAR_GRAPHQL_URL = "https://api.linear.app/graphql" - -ORGANIZATION_QUERY = """ -query { - organization { - name - } -} -""" - - -async def fetch_linear_organization_name(access_token: str) -> str | None: - """ - Fetch organization/workspace name from Linear GraphQL API. - - Args: - access_token: The Linear OAuth access token - - Returns: - Organization name or None if fetch fails - """ - try: - async with httpx.AsyncClient() as client: - response = await client.post( - LINEAR_GRAPHQL_URL, - headers={ - "Authorization": access_token, - "Content-Type": "application/json", - }, - json={"query": ORGANIZATION_QUERY}, - timeout=10.0, - ) - - if response.status_code == 200: - data = response.json() - org_name = data.get("data", {}).get("organization", {}).get("name") - if org_name: - logger.debug(f"Fetched Linear organization name: {org_name}") - return org_name - - logger.warning(f"Failed to fetch Linear org info: {response.status_code}") - return None - - except Exception as e: - logger.warning(f"Error fetching Linear organization name: {e!s}") - return None - diff --git a/surfsense_backend/app/routes/linear_add_connector_route.py b/surfsense_backend/app/routes/linear_add_connector_route.py index ca1d09568..db79afdcb 100644 --- a/surfsense_backend/app/routes/linear_add_connector_route.py +++ b/surfsense_backend/app/routes/linear_add_connector_route.py @@ -23,7 +23,7 @@ from app.db import ( User, get_async_session, ) -from app.connectors.linear_oauth import fetch_linear_organization_name +from app.connectors.linear_connector import fetch_linear_organization_name from app.schemas.linear_auth_credentials import LinearAuthCredentialsBase from app.users import current_active_user from app.utils.connector_naming import check_duplicate_connector, generate_unique_connector_name @@ -454,4 +454,4 @@ async def refresh_linear_token( logger.error(f"Failed to refresh Linear token: {e!s}", exc_info=True) raise HTTPException( status_code=500, detail=f"Failed to refresh Linear token: {e!s}" - ) from e + ) from e \ No newline at end of file From 5f0013c1098547ce612fedea8750fc18c6edd98e Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Wed, 7 Jan 2026 13:33:05 +0200 Subject: [PATCH 63/75] fix: restore duplicate check for non-OAuth connectors --- .../app/connectors/linear_connector.py | 5 +++-- .../routes/airtable_add_connector_route.py | 1 - .../google_calendar_add_connector_route.py | 2 +- .../google_drive_add_connector_route.py | 2 +- .../google_gmail_add_connector_route.py | 2 +- .../app/routes/linear_add_connector_route.py | 2 +- .../routes/search_source_connectors_routes.py | 22 ++++++++++++++++--- 7 files changed, 26 insertions(+), 10 deletions(-) diff --git a/surfsense_backend/app/connectors/linear_connector.py b/surfsense_backend/app/connectors/linear_connector.py index 61980e7ba..b8206a40d 100644 --- a/surfsense_backend/app/connectors/linear_connector.py +++ b/surfsense_backend/app/connectors/linear_connector.py @@ -16,7 +16,6 @@ from sqlalchemy.future import select from app.config import config from app.db import SearchSourceConnector -from app.routes.linear_add_connector_route import refresh_linear_token from app.schemas.linear_auth_credentials import LinearAuthCredentialsBase from app.utils.oauth_security import TokenEncryption @@ -169,6 +168,9 @@ class LinearConnector: f"Connector {self._connector_id} not found; cannot refresh token." ) + # Lazy import to avoid circular dependency + from app.routes.linear_add_connector_route import refresh_linear_token + # Refresh token connector = await refresh_linear_token(self._session, connector) @@ -640,4 +642,3 @@ class LinearConnector: return dt.strftime("%Y-%m-%d %H:%M:%S") except ValueError: return iso_date - diff --git a/surfsense_backend/app/routes/airtable_add_connector_route.py b/surfsense_backend/app/routes/airtable_add_connector_route.py index 92fcbc67e..5fa8180bf 100644 --- a/surfsense_backend/app/routes/airtable_add_connector_route.py +++ b/surfsense_backend/app/routes/airtable_add_connector_route.py @@ -277,7 +277,6 @@ async def airtable_callback( status_code=400, detail="No access token received from Airtable" ) - # Fetch user email before encrypting credentials user_email = await fetch_airtable_user_email(access_token) 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 a721b62c1..a1292fa43 100644 --- a/surfsense_backend/app/routes/google_calendar_add_connector_route.py +++ b/surfsense_backend/app/routes/google_calendar_add_connector_route.py @@ -174,7 +174,7 @@ async def calendar_callback( creds = flow.credentials creds_dict = json.loads(creds.to_json()) - # Fetch user email before encrypting credentials + # Fetch user email user_email = fetch_google_user_email(creds) # Encrypt sensitive credentials before storing diff --git a/surfsense_backend/app/routes/google_drive_add_connector_route.py b/surfsense_backend/app/routes/google_drive_add_connector_route.py index 1b02543d3..30a46a618 100644 --- a/surfsense_backend/app/routes/google_drive_add_connector_route.py +++ b/surfsense_backend/app/routes/google_drive_add_connector_route.py @@ -229,7 +229,7 @@ async def drive_callback( creds = flow.credentials creds_dict = json.loads(creds.to_json()) - # Fetch user email before encrypting credentials + # Fetch user email user_email = fetch_google_user_email(creds) # Encrypt sensitive credentials before storing diff --git a/surfsense_backend/app/routes/google_gmail_add_connector_route.py b/surfsense_backend/app/routes/google_gmail_add_connector_route.py index 4a7631919..9919894f3 100644 --- a/surfsense_backend/app/routes/google_gmail_add_connector_route.py +++ b/surfsense_backend/app/routes/google_gmail_add_connector_route.py @@ -205,7 +205,7 @@ async def gmail_callback( creds = flow.credentials creds_dict = json.loads(creds.to_json()) - # Fetch user email before encrypting credentials + # Fetch user email user_email = fetch_google_user_email(creds) # Encrypt sensitive credentials before storing diff --git a/surfsense_backend/app/routes/linear_add_connector_route.py b/surfsense_backend/app/routes/linear_add_connector_route.py index db79afdcb..ce5cdbfb3 100644 --- a/surfsense_backend/app/routes/linear_add_connector_route.py +++ b/surfsense_backend/app/routes/linear_add_connector_route.py @@ -242,7 +242,7 @@ async def linear_callback( status_code=400, detail="No access token received from Linear" ) - # Fetch organization name before encrypting credentials + # Fetch organization name org_name = await fetch_linear_organization_name(access_token) # Calculate expiration time (UTC, tz-aware) diff --git a/surfsense_backend/app/routes/search_source_connectors_routes.py b/surfsense_backend/app/routes/search_source_connectors_routes.py index a92be5f6e..58a50a6f8 100644 --- a/surfsense_backend/app/routes/search_source_connectors_routes.py +++ b/surfsense_backend/app/routes/search_source_connectors_routes.py @@ -7,7 +7,8 @@ PUT /search-source-connectors/{connector_id} - Update a specific connector DELETE /search-source-connectors/{connector_id} - Delete a specific connector POST /search-source-connectors/{connector_id}/index - Index content from a connector to a search space -Note: Each search space can have multiple connectors of the same type per user (uniqueness is no longer enforced, you may connect several accounts of the same type). +Note: OAuth connectors (Gmail, Drive, Slack, etc.) support multiple accounts per search space. +Non-OAuth connectors (BookStack, GitHub, etc.) are limited to one per search space. """ import logging @@ -111,7 +112,7 @@ async def create_search_source_connector( Create a new search source connector. Requires CONNECTORS_CREATE permission. - Each search space can have multiple connectors of the same type (e.g., multiple Gmail, Slack, etc. accounts). + Each search space can have only one connector of each type (based on search_space_id and connector_type). The config must contain the appropriate keys for the connector type. """ try: @@ -124,6 +125,21 @@ async def create_search_source_connector( "You don't have permission to create connectors in this search space", ) + # Check if a connector with the same type already exists for this search space + # (for non-OAuth connectors that don't support multiple accounts) + result = await session.execute( + select(SearchSourceConnector).filter( + SearchSourceConnector.search_space_id == search_space_id, + SearchSourceConnector.connector_type == connector.connector_type, + ) + ) + existing_connector = result.scalars().first() + if existing_connector: + raise HTTPException( + status_code=409, + detail=f"A connector with type {connector.connector_type} already exists in this search space.", + ) + # Prepare connector data connector_data = connector.model_dump() @@ -169,7 +185,7 @@ async def create_search_source_connector( await session.rollback() raise HTTPException( status_code=409, - detail=f"Integrity error: {e!s}", + detail=f"Integrity error: A connector with this type already exists in this search space. {e!s}", ) from e except HTTPException: await session.rollback() From 4de28152d5ec5d88bd12bafd1438f9e6035f7dc0 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Wed, 7 Jan 2026 15:18:31 +0200 Subject: [PATCH 64/75] fix: connector card UI improvements --- .../connector-popup/components/connector-card.tsx | 12 ++++++++++-- .../connector-popup/tabs/active-connectors-tab.tsx | 10 ++++------ .../connector-popup/tabs/all-connectors-tab.tsx | 1 + 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/surfsense_web/components/assistant-ui/connector-popup/components/connector-card.tsx b/surfsense_web/components/assistant-ui/connector-popup/components/connector-card.tsx index faf20e055..3b1c41cdf 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/components/connector-card.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/components/connector-card.tsx @@ -17,6 +17,7 @@ interface ConnectorCardProps { isConnected?: boolean; isConnecting?: boolean; documentCount?: number; + accountCount?: number; lastIndexedAt?: string | null; isIndexing?: boolean; activeTask?: LogActiveTask; @@ -96,6 +97,7 @@ export const ConnectorCard: FC = ({ isConnected = false, isConnecting = false, documentCount, + accountCount, lastIndexedAt, isIndexing = false, activeTask, @@ -154,8 +156,14 @@ export const ConnectorCard: FC = ({
    {getStatusContent()}
    {isConnected && documentCount !== undefined && ( -

    - {formatDocumentCount(documentCount)} +

    + {formatDocumentCount(documentCount)} + {accountCount !== undefined && accountCount > 0 && ( + <> + + {accountCount} {accountCount === 1 ? "Account" : "Accounts"} + + )}

    )}
    diff --git a/surfsense_web/components/assistant-ui/connector-popup/tabs/active-connectors-tab.tsx b/surfsense_web/components/assistant-ui/connector-popup/tabs/active-connectors-tab.tsx index d2f8a7fa6..2f0e31106 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/tabs/active-connectors-tab.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/tabs/active-connectors-tab.tsx @@ -200,10 +200,6 @@ export const ActiveConnectorsTab: FC = ({ : "bg-slate-400/5 dark:bg-white/5 hover:bg-slate-400/10 dark:hover:bg-white/10" )} > - {/* Account count badge */} -
    - {accountCount > 99 ? "99+" : accountCount} {accountCount === 1 ? "Account" : "Accounts"} -
    = ({ : "Never indexed"}

    )} -

    - {formatDocumentCount(documentCount)} +

    + {formatDocumentCount(documentCount)} + + {accountCount} {accountCount === 1 ? "Account" : "Accounts"}

    {/* Bottom fade shadow */} diff --git a/surfsense_web/components/assistant-ui/connector-popup/components/connector-card.tsx b/surfsense_web/components/assistant-ui/connector-popup/components/connector-card.tsx index 3b1c41cdf..e8fe6da33 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/components/connector-card.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/components/connector-card.tsx @@ -161,7 +161,9 @@ export const ConnectorCard: FC = ({ {accountCount !== undefined && accountCount > 0 && ( <> - {accountCount} {accountCount === 1 ? "Account" : "Accounts"} + + {accountCount} {accountCount === 1 ? "Account" : "Accounts"} + )}

    diff --git a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-edit-view.tsx b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-edit-view.tsx index e09bdea90..bdfe9af77 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-edit-view.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-edit-view.tsx @@ -148,7 +148,9 @@ export const ConnectorEditView: FC = ({ {getConnectorIcon(connector.connector_type, "size-7")}
    -

    {connector.name}

    +

    + {connector.name} +

    Manage your connector settings and sync configuration

    diff --git a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/indexing-configuration-view.tsx b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/indexing-configuration-view.tsx index 2dcadf459..8f4a29e61 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/indexing-configuration-view.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/indexing-configuration-view.tsx @@ -1,17 +1,17 @@ "use client"; import { ArrowLeft, Check, Info, Loader2 } from "lucide-react"; -import { type FC, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useSearchParams } from "next/navigation"; +import { type FC, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Button } from "@/components/ui/button"; import type { SearchSourceConnector } from "@/contracts/types/connector.types"; +import { getConnectorTypeDisplay } from "@/lib/connectors/utils"; import { cn } from "@/lib/utils"; import { DateRangeSelector } from "../../components/date-range-selector"; import { PeriodicSyncConfig } from "../../components/periodic-sync-config"; -import { OAUTH_CONNECTORS, type IndexingConfigState } from "../../constants/connector-constants"; -import { getConnectorConfigComponent } from "../index"; -import { getConnectorTypeDisplay } from "@/lib/connectors/utils"; +import { type IndexingConfigState, OAUTH_CONNECTORS } from "../../constants/connector-constants"; import { getConnectorDisplayName } from "../../tabs/all-connectors-tab"; +import { getConnectorConfigComponent } from "../index"; interface IndexingConfigurationViewProps { config: IndexingConfigState; @@ -121,7 +121,12 @@ export const IndexingConfigurationView: FC = ({
    - {getConnectorTypeDisplay(connector?.connector_type || "")} Connected ! {getConnectorDisplayName(connector?.name || "")} + + {getConnectorTypeDisplay(connector?.connector_type || "")} Connected ! + {" "} + + {getConnectorDisplayName(connector?.name || "")} +

    Configure when to start syncing your data diff --git a/surfsense_web/components/assistant-ui/connector-popup/hooks/use-connector-dialog.ts b/surfsense_web/components/assistant-ui/connector-popup/hooks/use-connector-dialog.ts index 1bfef9c43..2c8248255 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/hooks/use-connector-dialog.ts +++ b/surfsense_web/components/assistant-ui/connector-popup/hooks/use-connector-dialog.ts @@ -243,7 +243,14 @@ export const useConnectorDialog = () => { console.warn("Invalid connector popup query params:", error); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [searchParams, allConnectors, editingConnector, indexingConfig, connectingConnectorType, viewingAccountsType]); + }, [ + searchParams, + allConnectors, + editingConnector, + indexingConfig, + connectingConnectorType, + viewingAccountsType, + ]); // Detect OAuth success / Failure and transition to config view useEffect(() => { @@ -292,9 +299,7 @@ export const useConnectorDialog = () => { let newConnector: SearchSourceConnector | undefined; if (params.connectorId) { const connectorId = parseInt(params.connectorId, 10); - newConnector = result.data.find( - (c: SearchSourceConnector) => c.id === connectorId - ); + newConnector = result.data.find((c: SearchSourceConnector) => c.id === connectorId); } else { newConnector = result.data.find( (c: SearchSourceConnector) => c.connector_type === oauthConnector.connectorType @@ -737,7 +742,6 @@ export const useConnectorDialog = () => { router.replace(url.pathname + url.search, { scroll: false }); }, [router]); - // Handle starting indexing const handleStartIndexing = useCallback( async (refreshConnectors: () => void) => { diff --git a/surfsense_web/components/assistant-ui/connector-popup/tabs/active-connectors-tab.tsx b/surfsense_web/components/assistant-ui/connector-popup/tabs/active-connectors-tab.tsx index 2f0e31106..7f1bd28f0 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/tabs/active-connectors-tab.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/tabs/active-connectors-tab.tsx @@ -83,7 +83,9 @@ export const ActiveConnectorsTab: FC = ({ }; // Get most recent last indexed date from a list of connectors - const getMostRecentLastIndexed = (connectorsList: SearchSourceConnector[]): string | undefined => { + const getMostRecentLastIndexed = ( + connectorsList: SearchSourceConnector[] + ): string | undefined => { return connectorsList.reduce((latest, c) => { if (!c.last_indexed_at) return latest; if (!latest) return c.last_indexed_at; @@ -131,20 +133,27 @@ export const ActiveConnectorsTab: FC = ({ const getOAuthConnectorTypeInfo = (connectorType: string) => { const oauthConnector = OAUTH_CONNECTORS.find((c) => c.connectorType === connectorType); return { - title: oauthConnector?.title || connectorType.replace(/_/g, " ").replace(/connector/gi, "").trim(), + title: + oauthConnector?.title || + connectorType + .replace(/_/g, " ") + .replace(/connector/gi, "") + .trim(), }; }; // Filter OAuth connector types based on search query - const filteredOAuthConnectorTypes = Object.entries(oauthConnectorsByType).filter(([connectorType]) => { - if (!searchQuery) return true; - const searchLower = searchQuery.toLowerCase(); - const { title } = getOAuthConnectorTypeInfo(connectorType); - return ( - title.toLowerCase().includes(searchLower) || - connectorType.toLowerCase().includes(searchLower) - ); - }); + const filteredOAuthConnectorTypes = Object.entries(oauthConnectorsByType).filter( + ([connectorType]) => { + if (!searchQuery) return true; + const searchLower = searchQuery.toLowerCase(); + const { title } = getOAuthConnectorTypeInfo(connectorType); + return ( + title.toLowerCase().includes(searchLower) || + connectorType.toLowerCase().includes(searchLower) + ); + } + ); // Filter non-OAuth connectors based on search query const filteredNonOAuthConnectors = nonOauthConnectors.filter((connector) => { @@ -156,7 +165,8 @@ export const ActiveConnectorsTab: FC = ({ ); }); - const hasActiveConnectors = filteredOAuthConnectorTypes.length > 0 || filteredNonOAuthConnectors.length > 0; + const hasActiveConnectors = + filteredOAuthConnectorTypes.length > 0 || filteredNonOAuthConnectors.length > 0; return ( @@ -172,8 +182,8 @@ export const ActiveConnectorsTab: FC = ({ {/* OAuth Connectors - Grouped by Type */} {filteredOAuthConnectorTypes.map(([connectorType, typeConnectors]) => { const { title } = getOAuthConnectorTypeInfo(connectorType); - const isAnyIndexing = typeConnectors.some( - (c: SearchSourceConnector) => indexingConnectorIds.has(c.id) + const isAnyIndexing = typeConnectors.some((c: SearchSourceConnector) => + indexingConnectorIds.has(c.id) ); const documentCount = getDocumentCountForConnector( connectorType, @@ -211,9 +221,7 @@ export const ActiveConnectorsTab: FC = ({ {getConnectorIcon(connectorType, "size-6")}

    -

    - {title} -

    +

    {title}

    {isAnyIndexing ? (

    @@ -229,7 +237,9 @@ export const ActiveConnectorsTab: FC = ({

    {formatDocumentCount(documentCount)} - {accountCount} {accountCount === 1 ? "Account" : "Accounts"} + + {accountCount} {accountCount === 1 ? "Account" : "Accounts"} +

    ); }; - diff --git a/surfsense_web/components/assistant-ui/document-upload-popup.tsx b/surfsense_web/components/assistant-ui/document-upload-popup.tsx index da3b820e5..6ac1ec979 100644 --- a/surfsense_web/components/assistant-ui/document-upload-popup.tsx +++ b/surfsense_web/components/assistant-ui/document-upload-popup.tsx @@ -1,7 +1,7 @@ "use client"; -import { Upload } from "lucide-react"; import { useAtomValue } from "jotai"; +import { Upload } from "lucide-react"; import { useRouter } from "next/navigation"; import { createContext, diff --git a/surfsense_web/components/markdown-viewer.tsx b/surfsense_web/components/markdown-viewer.tsx index 407adba7a..93e3f26e1 100644 --- a/surfsense_web/components/markdown-viewer.tsx +++ b/surfsense_web/components/markdown-viewer.tsx @@ -1,5 +1,5 @@ import Image from "next/image"; -import { type StreamdownProps, Streamdown } from "streamdown"; +import { Streamdown, type StreamdownProps } from "streamdown"; import { cn } from "@/lib/utils"; interface MarkdownViewerProps { diff --git a/surfsense_web/content/docs/connectors/meta.json b/surfsense_web/content/docs/connectors/meta.json index 2515bc7d8..82e04e44f 100644 --- a/surfsense_web/content/docs/connectors/meta.json +++ b/surfsense_web/content/docs/connectors/meta.json @@ -20,4 +20,3 @@ ], "defaultOpen": true } - From 9841bdda7213eb0bafcf7a77a364ad38693d8cce Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Wed, 7 Jan 2026 15:27:54 +0200 Subject: [PATCH 66/75] style: format backend with ruff --- .../versions/57_allow_multiple_connectors_per_type.py | 8 +++++--- .../58_unique_connector_name_per_space_user.py | 8 +++++--- surfsense_backend/app/connectors/airtable_connector.py | 4 +++- .../app/routes/airtable_add_connector_route.py | 9 +++++---- .../app/routes/confluence_add_connector_route.py | 3 +-- .../app/routes/discord_add_connector_route.py | 3 +-- .../app/routes/google_calendar_add_connector_route.py | 6 ++++-- .../app/routes/google_drive_add_connector_route.py | 5 ++++- .../app/routes/google_gmail_add_connector_route.py | 6 ++++-- .../app/routes/jira_add_connector_route.py | 3 +-- .../app/routes/linear_add_connector_route.py | 10 ++++++---- .../app/routes/notion_add_connector_route.py | 3 +-- .../app/routes/slack_add_connector_route.py | 1 - surfsense_backend/app/utils/connector_naming.py | 9 ++++++--- 14 files changed, 46 insertions(+), 32 deletions(-) diff --git a/surfsense_backend/alembic/versions/57_allow_multiple_connectors_per_type.py b/surfsense_backend/alembic/versions/57_allow_multiple_connectors_per_type.py index bd2fccf72..25558f42e 100644 --- a/surfsense_backend/alembic/versions/57_allow_multiple_connectors_per_type.py +++ b/surfsense_backend/alembic/versions/57_allow_multiple_connectors_per_type.py @@ -7,6 +7,7 @@ Create Date: 2026-01-06 12:00:00.000000 """ from collections.abc import Sequence + from alembic import op # revision identifiers, used by Alembic. @@ -17,6 +18,7 @@ depends_on: str | Sequence[str] | None = None from sqlalchemy import text + def upgrade() -> None: connection = op.get_bind() constraint_exists = connection.execute( @@ -31,9 +33,10 @@ def upgrade() -> None: op.drop_constraint( "uq_searchspace_user_connector_type", "search_source_connectors", - type_="unique" + type_="unique", ) + def downgrade() -> None: connection = op.get_bind() constraint_exists = connection.execute( @@ -48,6 +51,5 @@ def downgrade() -> None: op.create_unique_constraint( "uq_searchspace_user_connector_type", "search_source_connectors", - ["search_space_id", "user_id", "connector_type"] + ["search_space_id", "user_id", "connector_type"], ) - diff --git a/surfsense_backend/alembic/versions/58_unique_connector_name_per_space_user.py b/surfsense_backend/alembic/versions/58_unique_connector_name_per_space_user.py index b840af267..7c35ab1d8 100644 --- a/surfsense_backend/alembic/versions/58_unique_connector_name_per_space_user.py +++ b/surfsense_backend/alembic/versions/58_unique_connector_name_per_space_user.py @@ -8,6 +8,7 @@ Create Date: 2026-01-06 14:00:00.000000 """ from collections.abc import Sequence + from alembic import op revision: str = "58" @@ -17,6 +18,7 @@ depends_on: str | Sequence[str] | None = None from sqlalchemy import text + def upgrade() -> None: connection = op.get_bind() constraint_exists = connection.execute( @@ -31,9 +33,10 @@ def upgrade() -> None: op.create_unique_constraint( "uq_searchspace_user_connector_name", "search_source_connectors", - ["search_space_id", "user_id", "name"] + ["search_space_id", "user_id", "name"], ) + def downgrade() -> None: connection = op.get_bind() constraint_exists = connection.execute( @@ -48,6 +51,5 @@ def downgrade() -> None: op.drop_constraint( "uq_searchspace_user_connector_name", "search_source_connectors", - type_="unique" + type_="unique", ) - diff --git a/surfsense_backend/app/connectors/airtable_connector.py b/surfsense_backend/app/connectors/airtable_connector.py index 30a366cdd..8264f4bfa 100644 --- a/surfsense_backend/app/connectors/airtable_connector.py +++ b/surfsense_backend/app/connectors/airtable_connector.py @@ -414,7 +414,9 @@ async def fetch_airtable_user_email(access_token: str) -> str | None: logger.debug(f"Fetched Airtable user email: {email}") return email - logger.warning(f"Failed to fetch Airtable user info: {response.status_code}") + logger.warning( + f"Failed to fetch Airtable user info: {response.status_code}" + ) return None except Exception as e: diff --git a/surfsense_backend/app/routes/airtable_add_connector_route.py b/surfsense_backend/app/routes/airtable_add_connector_route.py index 5fa8180bf..5efa63e59 100644 --- a/surfsense_backend/app/routes/airtable_add_connector_route.py +++ b/surfsense_backend/app/routes/airtable_add_connector_route.py @@ -11,19 +11,21 @@ from fastapi.responses import RedirectResponse 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.connectors.airtable_connector import fetch_airtable_user_email from app.db import ( SearchSourceConnector, SearchSourceConnectorType, User, get_async_session, ) -from app.connectors.airtable_connector import fetch_airtable_user_email from app.schemas.airtable_auth_credentials import AirtableAuthCredentialsBase from app.users import current_active_user -from app.utils.connector_naming import check_duplicate_connector, generate_unique_connector_name +from app.utils.connector_naming import ( + check_duplicate_connector, + generate_unique_connector_name, +) from app.utils.oauth_security import OAuthStateManager, TokenEncryption logger = logging.getLogger(__name__) @@ -279,7 +281,6 @@ async def airtable_callback( user_email = await fetch_airtable_user_email(access_token) - # Calculate expiration time (UTC, tz-aware) expires_at = None if token_json.get("expires_in"): diff --git a/surfsense_backend/app/routes/confluence_add_connector_route.py b/surfsense_backend/app/routes/confluence_add_connector_route.py index 56abf62ce..6c5830b17 100644 --- a/surfsense_backend/app/routes/confluence_add_connector_route.py +++ b/surfsense_backend/app/routes/confluence_add_connector_route.py @@ -14,7 +14,6 @@ from fastapi.responses import RedirectResponse 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 ( @@ -25,12 +24,12 @@ from app.db import ( ) from app.schemas.atlassian_auth_credentials import AtlassianAuthCredentialsBase from app.users import current_active_user -from app.utils.oauth_security import OAuthStateManager, TokenEncryption from app.utils.connector_naming import ( check_duplicate_connector, extract_identifier_from_credentials, generate_unique_connector_name, ) +from app.utils.oauth_security import OAuthStateManager, TokenEncryption logger = logging.getLogger(__name__) diff --git a/surfsense_backend/app/routes/discord_add_connector_route.py b/surfsense_backend/app/routes/discord_add_connector_route.py index 0bda191c6..1d8b40fcf 100644 --- a/surfsense_backend/app/routes/discord_add_connector_route.py +++ b/surfsense_backend/app/routes/discord_add_connector_route.py @@ -14,7 +14,6 @@ from fastapi.responses import RedirectResponse 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 ( @@ -25,12 +24,12 @@ from app.db import ( ) from app.schemas.discord_auth_credentials import DiscordAuthCredentialsBase from app.users import current_active_user -from app.utils.oauth_security import OAuthStateManager, TokenEncryption from app.utils.connector_naming import ( check_duplicate_connector, extract_identifier_from_credentials, generate_unique_connector_name, ) +from app.utils.oauth_security import OAuthStateManager, TokenEncryption logger = logging.getLogger(__name__) 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 a1292fa43..08e5c2f04 100644 --- a/surfsense_backend/app/routes/google_calendar_add_connector_route.py +++ b/surfsense_backend/app/routes/google_calendar_add_connector_route.py @@ -12,7 +12,6 @@ from google_auth_oauthlib.flow import Flow 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.connectors.google_gmail_connector import fetch_google_user_email @@ -23,7 +22,10 @@ from app.db import ( get_async_session, ) from app.users import current_active_user -from app.utils.connector_naming import check_duplicate_connector, generate_unique_connector_name +from app.utils.connector_naming import ( + check_duplicate_connector, + generate_unique_connector_name, +) from app.utils.oauth_security import OAuthStateManager, TokenEncryption logger = logging.getLogger(__name__) diff --git a/surfsense_backend/app/routes/google_drive_add_connector_route.py b/surfsense_backend/app/routes/google_drive_add_connector_route.py index 30a46a618..e15aed762 100644 --- a/surfsense_backend/app/routes/google_drive_add_connector_route.py +++ b/surfsense_backend/app/routes/google_drive_add_connector_route.py @@ -37,7 +37,10 @@ from app.db import ( get_async_session, ) from app.users import current_active_user -from app.utils.connector_naming import check_duplicate_connector, generate_unique_connector_name +from app.utils.connector_naming import ( + check_duplicate_connector, + generate_unique_connector_name, +) from app.utils.oauth_security import OAuthStateManager, TokenEncryption # Relax token scope validation for Google OAuth diff --git a/surfsense_backend/app/routes/google_gmail_add_connector_route.py b/surfsense_backend/app/routes/google_gmail_add_connector_route.py index 9919894f3..19fa019ce 100644 --- a/surfsense_backend/app/routes/google_gmail_add_connector_route.py +++ b/surfsense_backend/app/routes/google_gmail_add_connector_route.py @@ -12,7 +12,6 @@ from google_auth_oauthlib.flow import Flow 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.connectors.google_gmail_connector import fetch_google_user_email @@ -23,7 +22,10 @@ from app.db import ( get_async_session, ) from app.users import current_active_user -from app.utils.connector_naming import check_duplicate_connector, generate_unique_connector_name +from app.utils.connector_naming import ( + check_duplicate_connector, + generate_unique_connector_name, +) from app.utils.oauth_security import OAuthStateManager, TokenEncryption logger = logging.getLogger(__name__) diff --git a/surfsense_backend/app/routes/jira_add_connector_route.py b/surfsense_backend/app/routes/jira_add_connector_route.py index 744cf7fd4..fb66f4da7 100644 --- a/surfsense_backend/app/routes/jira_add_connector_route.py +++ b/surfsense_backend/app/routes/jira_add_connector_route.py @@ -15,7 +15,6 @@ from fastapi.responses import RedirectResponse 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 ( @@ -26,12 +25,12 @@ from app.db import ( ) from app.schemas.atlassian_auth_credentials import AtlassianAuthCredentialsBase from app.users import current_active_user -from app.utils.oauth_security import OAuthStateManager, TokenEncryption from app.utils.connector_naming import ( check_duplicate_connector, extract_identifier_from_credentials, generate_unique_connector_name, ) +from app.utils.oauth_security import OAuthStateManager, TokenEncryption logger = logging.getLogger(__name__) diff --git a/surfsense_backend/app/routes/linear_add_connector_route.py b/surfsense_backend/app/routes/linear_add_connector_route.py index ce5cdbfb3..fc9501bfb 100644 --- a/surfsense_backend/app/routes/linear_add_connector_route.py +++ b/surfsense_backend/app/routes/linear_add_connector_route.py @@ -14,19 +14,21 @@ from fastapi.responses import RedirectResponse 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.connectors.linear_connector import fetch_linear_organization_name from app.db import ( SearchSourceConnector, SearchSourceConnectorType, User, get_async_session, ) -from app.connectors.linear_connector import fetch_linear_organization_name from app.schemas.linear_auth_credentials import LinearAuthCredentialsBase from app.users import current_active_user -from app.utils.connector_naming import check_duplicate_connector, generate_unique_connector_name +from app.utils.connector_naming import ( + check_duplicate_connector, + generate_unique_connector_name, +) from app.utils.oauth_security import OAuthStateManager, TokenEncryption logger = logging.getLogger(__name__) @@ -454,4 +456,4 @@ async def refresh_linear_token( logger.error(f"Failed to refresh Linear token: {e!s}", exc_info=True) raise HTTPException( status_code=500, detail=f"Failed to refresh Linear token: {e!s}" - ) from e \ No newline at end of file + ) from e diff --git a/surfsense_backend/app/routes/notion_add_connector_route.py b/surfsense_backend/app/routes/notion_add_connector_route.py index c0331b4bc..aac821793 100644 --- a/surfsense_backend/app/routes/notion_add_connector_route.py +++ b/surfsense_backend/app/routes/notion_add_connector_route.py @@ -14,7 +14,6 @@ from fastapi.responses import RedirectResponse 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 ( @@ -25,12 +24,12 @@ from app.db import ( ) from app.schemas.notion_auth_credentials import NotionAuthCredentialsBase from app.users import current_active_user -from app.utils.oauth_security import OAuthStateManager, TokenEncryption from app.utils.connector_naming import ( check_duplicate_connector, extract_identifier_from_credentials, generate_unique_connector_name, ) +from app.utils.oauth_security import OAuthStateManager, TokenEncryption logger = logging.getLogger(__name__) diff --git a/surfsense_backend/app/routes/slack_add_connector_route.py b/surfsense_backend/app/routes/slack_add_connector_route.py index 6da7e5d24..62d2ccaaa 100644 --- a/surfsense_backend/app/routes/slack_add_connector_route.py +++ b/surfsense_backend/app/routes/slack_add_connector_route.py @@ -14,7 +14,6 @@ from fastapi.responses import RedirectResponse 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 ( diff --git a/surfsense_backend/app/utils/connector_naming.py b/surfsense_backend/app/utils/connector_naming.py index 17b791c34..f9f1fdd21 100644 --- a/surfsense_backend/app/utils/connector_naming.py +++ b/surfsense_backend/app/utils/connector_naming.py @@ -31,7 +31,9 @@ BASE_NAME_FOR_TYPE = { def get_base_name_for_type(connector_type: SearchSourceConnectorType) -> str: """Get a friendly display name for a connector type.""" - return BASE_NAME_FOR_TYPE.get(connector_type, connector_type.replace("_", " ").title()) + return BASE_NAME_FOR_TYPE.get( + connector_type, connector_type.replace("_", " ").title() + ) def extract_identifier_from_credentials( @@ -178,9 +180,10 @@ async def generate_unique_connector_name( return f"{base} - {identifier}" # Fallback: use counter for uniqueness - count = await count_connectors_of_type(session, connector_type, search_space_id, user_id) + count = await count_connectors_of_type( + session, connector_type, search_space_id, user_id + ) if count == 0: return base return f"{base} ({count + 1})" - From 31383ad0d76680e434192a16067d2fe87052564e Mon Sep 17 00:00:00 2001 From: Manoj Aggarwal Date: Wed, 7 Jan 2026 13:36:15 -0800 Subject: [PATCH 67/75] Minor modifications to documentation --- surfsense_backend/pyproject.toml | 10 +++- .../content/docs/manual-installation.mdx | 56 +++++++++++++++++-- 2 files changed, 61 insertions(+), 5 deletions(-) diff --git a/surfsense_backend/pyproject.toml b/surfsense_backend/pyproject.toml index ba1d69939..2edfc8fea 100644 --- a/surfsense_backend/pyproject.toml +++ b/surfsense_backend/pyproject.toml @@ -2,7 +2,6 @@ name = "surf-new-backend" version = "0.0.10" description = "SurfSense Backend" -readme = "README.md" requires-python = ">=3.12" dependencies = [ "alembic>=1.13.0", @@ -153,3 +152,12 @@ line-ending = "auto" known-first-party = ["app"] force-single-line = false combine-as-imports = true + +[tool.setuptools.packages.find] +where = ["."] +include = ["app*"] +exclude = ["alembic*"] + +[build-system] +requires = ["setuptools>=61.0", "wheel"] +build-backend = "setuptools.build_meta" diff --git a/surfsense_web/content/docs/manual-installation.mdx b/surfsense_web/content/docs/manual-installation.mdx index e7caf93a6..6961070c3 100644 --- a/surfsense_web/content/docs/manual-installation.mdx +++ b/surfsense_web/content/docs/manual-installation.mdx @@ -234,7 +234,7 @@ redis-cli ping In a new terminal window, start the Celery worker to handle background tasks: -**Linux/macOS/Windows:** +**If using uv:** ```bash # Make sure you're in the surfsense_backend directory @@ -244,13 +244,31 @@ cd surfsense_backend uv run celery -A celery_worker.celery_app worker --loglevel=info --concurrency=1 --pool=solo ``` +**If using pip/venv:** + +```bash +# Make sure you're in the surfsense_backend directory +cd surfsense_backend + +# Activate virtual environment +source .venv/bin/activate # Linux/macOS +# OR +.venv\Scripts\activate # Windows + +# Start Celery worker +celery -A celery_worker.celery_app worker --loglevel=info --concurrency=1 --pool=solo +``` + **Optional: Start Flower for monitoring Celery tasks:** In another terminal window: ```bash -# Start Flower (Celery monitoring tool) +# If using uv uv run celery -A celery_worker.celery_app flower --port=5555 + +# If using pip/venv (activate venv first) +celery -A celery_worker.celery_app flower --port=5555 ``` Access Flower at [http://localhost:5555](http://localhost:5555) to monitor your Celery tasks. @@ -259,7 +277,7 @@ Access Flower at [http://localhost:5555](http://localhost:5555) to monitor your In another new terminal window, start Celery Beat to enable periodic tasks (like scheduled connector indexing): -**Linux/macOS/Windows:** +**If using uv:** ```bash # Make sure you're in the surfsense_backend directory @@ -269,13 +287,28 @@ cd surfsense_backend uv run celery -A celery_worker.celery_app beat --loglevel=info ``` +**If using pip/venv:** + +```bash +# Make sure you're in the surfsense_backend directory +cd surfsense_backend + +# Activate virtual environment +source .venv/bin/activate # Linux/macOS +# OR +.venv\Scripts\activate # Windows + +# Start Celery Beat +celery -A celery_worker.celery_app beat --loglevel=info +``` + **Important**: Celery Beat is required for the periodic indexing functionality to work. Without it, scheduled connector tasks won't run automatically. The schedule interval can be configured using the `SCHEDULE_CHECKER_INTERVAL` environment variable. ### 6. Run the Backend Start the backend server: -**Linux/macOS/Windows:** +**If using uv:** ```bash # Run without hot reloading @@ -285,6 +318,21 @@ uv run main.py uv run main.py --reload ``` +**If using pip/venv:** + +```bash +# Activate virtual environment if not already activated +source .venv/bin/activate # Linux/macOS +# OR +.venv\Scripts\activate # Windows + +# Run without hot reloading +python main.py + +# Or with hot reloading for development +python main.py --reload +``` + If everything is set up correctly, you should see output indicating the server is running on `http://localhost:8000`. ## Frontend Setup From f567fe7f3b74a68abce16a816881b67bdae50988 Mon Sep 17 00:00:00 2001 From: Manoj Aggarwal Date: Wed, 7 Jan 2026 13:42:52 -0800 Subject: [PATCH 68/75] Add both app and alembic explicitly --- surfsense_backend/pyproject.toml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/surfsense_backend/pyproject.toml b/surfsense_backend/pyproject.toml index 2edfc8fea..099f1e338 100644 --- a/surfsense_backend/pyproject.toml +++ b/surfsense_backend/pyproject.toml @@ -155,8 +155,7 @@ combine-as-imports = true [tool.setuptools.packages.find] where = ["."] -include = ["app*"] -exclude = ["alembic*"] +include = ["app*", "alembic*"] [build-system] requires = ["setuptools>=61.0", "wheel"] From 73eeb555b2ad276855c199edd9a996e493340a29 Mon Sep 17 00:00:00 2001 From: "DESKTOP-RTLN3BA\\$punk" Date: Wed, 7 Jan 2026 17:46:38 -0800 Subject: [PATCH 69/75] refactor: update launch configurations and enhance HeroSection with Google login functionality --- .vscode/launch.json | 94 +++++++++++++-- .../components/homepage/hero-section.tsx | 110 +++++++++++++++--- 2 files changed, 181 insertions(+), 23 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index dfe20d832..ad7f04bd0 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -5,7 +5,7 @@ "version": "0.2.0", "configurations": [ { - "name": "Python Debugger: UV Run with Reload", + "name": "Backend: FastAPI", "type": "debugpy", "request": "launch", "module": "uvicorn", @@ -25,7 +25,7 @@ "python": "${command:python.interpreterPath}" }, { - "name": "Python Debugger: main.py (direct)", + "name": "Backend: FastAPI (main.py)", "type": "debugpy", "request": "launch", "program": "${workspaceFolder}/surfsense_backend/main.py", @@ -34,17 +34,95 @@ "cwd": "${workspaceFolder}/surfsense_backend" }, { - "name": "Python Debugger: Chat DeepAgent", + "name": "Frontend: Next.js", + "type": "node", + "request": "launch", + "cwd": "${workspaceFolder}/surfsense_web", + "runtimeExecutable": "npm", + "runtimeArgs": ["run", "dev"], + "console": "integratedTerminal", + "serverReadyAction": { + "pattern": "- Local:.+(https?://.+)", + "uriFormat": "%s", + "action": "debugWithChrome" + } + }, + { + "name": "Frontend: Next.js (Server-Side Debug)", + "type": "node", + "request": "launch", + "cwd": "${workspaceFolder}/surfsense_web", + "runtimeExecutable": "npm", + "runtimeArgs": ["run", "debug:server"], + "console": "integratedTerminal", + "serverReadyAction": { + "pattern": "- Local:.+(https?://.+)", + "uriFormat": "%s", + "action": "debugWithChrome" + } + }, + { + "name": "Celery: Worker", "type": "debugpy", "request": "launch", - "module": "app.agents.new_chat.chat_deepagent", + "module": "celery", + "args": [ + "-A", + "app.celery_app:celery_app", + "worker", + "--loglevel=info", + "--pool=solo" + ], "console": "integratedTerminal", "justMyCode": false, "cwd": "${workspaceFolder}/surfsense_backend", - "python": "${command:python.interpreterPath}", - "env": { - "PYTHONPATH": "${workspaceFolder}/surfsense_backend" + "python": "${command:python.interpreterPath}" + }, + { + "name": "Celery: Beat Scheduler", + "type": "debugpy", + "request": "launch", + "module": "celery", + "args": [ + "-A", + "app.celery_app:celery_app", + "beat", + "--loglevel=info" + ], + "console": "integratedTerminal", + "justMyCode": false, + "cwd": "${workspaceFolder}/surfsense_backend", + "python": "${command:python.interpreterPath}" + } + ], + "compounds": [ + { + "name": "Full Stack: Backend + Frontend + Celery", + "configurations": [ + "Backend: FastAPI", + "Frontend: Next.js", + "Celery: Worker", + "Celery: Beat Scheduler" + ], + "stopAll": true, + "presentation": { + "hidden": false, + "group": "Full Stack", + "order": 1 + } + }, + { + "name": "Full Stack: Backend + Frontend", + "configurations": [ + "Backend: FastAPI", + "Frontend: Next.js" + ], + "stopAll": true, + "presentation": { + "hidden": false, + "group": "Full Stack", + "order": 2 } } ] -} \ No newline at end of file +} diff --git a/surfsense_web/components/homepage/hero-section.tsx b/surfsense_web/components/homepage/hero-section.tsx index db7525881..4b76e1f7b 100644 --- a/surfsense_web/components/homepage/hero-section.tsx +++ b/surfsense_web/components/homepage/hero-section.tsx @@ -5,6 +5,29 @@ import Link from "next/link"; import React, { useEffect, useRef, useState } from "react"; import Balancer from "react-wrap-balancer"; import { cn } from "@/lib/utils"; +import { trackLoginAttempt } from "@/lib/posthog/events"; + +// Official Google "G" logo with brand colors +const GoogleLogo = ({ className }: { className?: string }) => ( + + + + + + +); export function HeroSection() { const containerRef = useRef(null); @@ -60,7 +83,7 @@ export function HeroSection() {

    The AI Workspace{" "} -
    +
    Built for Teams
    @@ -73,12 +96,7 @@ export function HeroSection() { your team.

    - - Get Started - + {/* { + trackLoginAttempt("google"); + window.location.href = `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/auth/google/authorize-redirect`; + }; + + if (isGoogleAuth) { + return ( + + {/* Animated gradient background on hover */} + + {/* Google logo with subtle animation */} + + + + Continue with Google + + ); + } + + return ( + + + Get Started + + + ); +} + const BackgroundGrids = () => { return (
    @@ -126,7 +206,7 @@ const BackgroundGrids = () => {
    -
    +
    @@ -237,7 +317,7 @@ const CollisionMechanism = React.forwardRef< repeatDelay: beamOptions.repeatDelay || 0, }} className={cn( - "absolute left-96 top-20 m-auto h-14 w-px rounded-full bg-gradient-to-t from-orange-500 via-yellow-500 to-transparent", + "absolute left-96 top-20 m-auto h-14 w-px rounded-full bg-linear-to-t from-orange-500 via-yellow-500 to-transparent", beamOptions.className )} /> @@ -276,7 +356,7 @@ const Explosion = ({ ...props }: React.HTMLProps) => { animate={{ opacity: [0, 1, 0] }} exit={{ opacity: 0 }} transition={{ duration: 1, ease: "easeOut" }} - className="absolute -inset-x-10 top-0 m-auto h-[4px] w-10 rounded-full bg-gradient-to-r from-transparent via-orange-500 to-transparent blur-sm" + className="absolute -inset-x-10 top-0 m-auto h-[4px] w-10 rounded-full bg-linear-to-r from-transparent via-orange-500 to-transparent blur-sm" > {spans.map((span) => ( ) => { initial={{ x: span.initialX, y: span.initialY, opacity: 1 }} animate={{ x: span.directionX, y: span.directionY, opacity: 0 }} transition={{ duration: Math.random() * 1.5 + 0.5, ease: "easeOut" }} - className="absolute h-1 w-1 rounded-full bg-gradient-to-b from-orange-500 to-yellow-500" + className="absolute h-1 w-1 rounded-full bg-linear-to-b from-orange-500 to-yellow-500" /> ))}
    @@ -307,11 +387,11 @@ const GridLineVertical = ({ className, offset }: { className?: string; offset?: } as React.CSSProperties } className={cn( - "absolute top-[calc(var(--offset)/2*-1)] h-[calc(100%+var(--offset))] w-[var(--width)]", + "absolute top-[calc(var(--offset)/2*-1)] h-[calc(100%+var(--offset))] w-(--width)", "bg-[linear-gradient(to_bottom,var(--color),var(--color)_50%,transparent_0,transparent)]", - "[background-size:var(--width)_var(--height)]", - "[mask:linear-gradient(to_top,var(--background)_var(--fade-stop),transparent),_linear-gradient(to_bottom,var(--background)_var(--fade-stop),transparent),_linear-gradient(black,black)]", - "[mask-composite:exclude]", + "bg-size-[var(--width)_var(--height)]", + "[mask:linear-gradient(to_top,var(--background)_var(--fade-stop),transparent),linear-gradient(to_bottom,var(--background)_var(--fade-stop),transparent),linear-gradient(black,black)]", + "mask-exclude", "z-30", "dark:bg-[linear-gradient(to_bottom,var(--color-dark),var(--color-dark)_50%,transparent_0,transparent)]", className From 48fc70a08b144479aa037448ccd068dfa2420409 Mon Sep 17 00:00:00 2001 From: "DESKTOP-RTLN3BA\\$punk" Date: Wed, 7 Jan 2026 19:07:06 -0800 Subject: [PATCH 70/75] chore: cleanup --- surfsense_backend/.env.example | 7 +- .../app/services/docling_service.py | 36 - surfsense_backend/app/services/llm_service.py | 55 +- .../app/services/query_service.py | 114 --- .../app/utils/document_converters.py | 82 --- surfsense_web/components/copy-button.tsx | 38 - .../EditConnectorLoadingSkeleton.tsx | 22 - .../editConnector/EditConnectorNameForm.tsx | 28 - .../EditGitHubConnectorConfig.tsx | 189 ----- .../editConnector/EditSimpleTokenForm.tsx | 49 -- .../components/editConnector/types.ts | 59 -- .../components/inference-params-editor.tsx | 1 + .../components/settings/llm-role-manager.tsx | 16 +- surfsense_web/components/sources/types.ts | 13 - .../tool-ui/shared/action-buttons.tsx | 41 -- .../components/tool-ui/shared/index.ts | 2 - .../components/tool-ui/shared/schema.ts | 23 - surfsense_web/hooks/use-chat.ts | 43 -- .../hooks/use-connector-edit-page.ts | 680 ------------------ surfsense_web/lib/auth-utils.ts | 41 -- surfsense_web/lib/utils.ts | 7 - surfsense_web/messages/en.json | 2 +- 22 files changed, 8 insertions(+), 1540 deletions(-) delete mode 100644 surfsense_backend/app/services/query_service.py delete mode 100644 surfsense_web/components/copy-button.tsx delete mode 100644 surfsense_web/components/editConnector/EditConnectorLoadingSkeleton.tsx delete mode 100644 surfsense_web/components/editConnector/EditConnectorNameForm.tsx delete mode 100644 surfsense_web/components/editConnector/EditGitHubConnectorConfig.tsx delete mode 100644 surfsense_web/components/editConnector/EditSimpleTokenForm.tsx delete mode 100644 surfsense_web/components/editConnector/types.ts delete mode 100644 surfsense_web/components/sources/types.ts delete mode 100644 surfsense_web/components/tool-ui/shared/action-buttons.tsx delete mode 100644 surfsense_web/components/tool-ui/shared/index.ts delete mode 100644 surfsense_web/components/tool-ui/shared/schema.ts delete mode 100644 surfsense_web/hooks/use-chat.ts delete mode 100644 surfsense_web/hooks/use-connector-edit-page.ts diff --git a/surfsense_backend/.env.example b/surfsense_backend/.env.example index f50cd6e10..ebbc8dc69 100644 --- a/surfsense_backend/.env.example +++ b/surfsense_backend/.env.example @@ -55,10 +55,11 @@ DISCORD_CLIENT_SECRET=your_discord_client_secret_here DISCORD_REDIRECT_URI=http://localhost:8000/api/v1/auth/discord/connector/callback DISCORD_BOT_TOKEN=your_bot_token_from_developer_portal -# Jira OAuth Configuration -JIRA_CLIENT_ID=your_jira_client_id_here -JIRA_CLIENT_SECRET=your_jira_client_secret_here +# Atlassian OAuth Configuration +ATLASSIAN_CLIENT_ID=V4Axk5VLcsAKJxffMjRGSHtlh17uVswl +ATLASSIAN_CLIENT_SECRET=ATOAmjcoJ_wpyr98F5nF9BVZFDtXpLHs53YnK8TVQhjJh2LuRPYrnDirBwW5lV5cWRbK9B430F02 JIRA_REDIRECT_URI=http://localhost:8000/api/v1/auth/jira/connector/callback +CONFLUENCE_REDIRECT_URI=http://localhost:8000/api/v1/auth/confluence/connector/callback # Linear OAuth Configuration LINEAR_CLIENT_ID=your_linear_client_id_here diff --git a/surfsense_backend/app/services/docling_service.py b/surfsense_backend/app/services/docling_service.py index a61148c6d..82eaf7f74 100644 --- a/surfsense_backend/app/services/docling_service.py +++ b/surfsense_backend/app/services/docling_service.py @@ -128,42 +128,6 @@ class DoclingService: logger.error(f"❌ Docling initialization failed: {e}") raise RuntimeError(f"Docling initialization failed: {e}") from e - def _configure_easyocr_local_models(self): - """Configure EasyOCR to use pre-downloaded local models.""" - try: - import os - - import easyocr - - # Set SSL environment for EasyOCR downloads - os.environ["CURL_CA_BUNDLE"] = "" - os.environ["REQUESTS_CA_BUNDLE"] = "" - - # Try to use local models first, fallback to download if needed - try: - reader = easyocr.Reader( - ["en"], - download_enabled=False, - model_storage_directory="/root/.EasyOCR/model", - ) - logger.info("✅ EasyOCR configured for local models") - return reader - except Exception: - # If local models fail, allow download with SSL bypass - logger.info( - "🔄 Local models failed, attempting download with SSL bypass..." - ) - reader = easyocr.Reader( - ["en"], - download_enabled=True, - model_storage_directory="/root/.EasyOCR/model", - ) - logger.info("✅ EasyOCR configured with downloaded models") - return reader - except Exception as e: - logger.warning(f"⚠️ EasyOCR configuration failed: {e}") - return None - async def process_document( self, file_path: str, filename: str | None = None ) -> dict[str, Any]: diff --git a/surfsense_backend/app/services/llm_service.py b/surfsense_backend/app/services/llm_service.py index 68dd167b5..33f073d61 100644 --- a/surfsense_backend/app/services/llm_service.py +++ b/surfsense_backend/app/services/llm_service.py @@ -342,40 +342,7 @@ async def get_document_summary_llm( ) -# Backward-compatible aliases (deprecated - will be removed in future versions) -async def get_user_llm_instance( - session: AsyncSession, user_id: str, search_space_id: int, role: str -) -> ChatLiteLLM | None: - """ - Deprecated: Use get_search_space_llm_instance instead. - LLM preferences are now stored at the search space level, not per-user. - """ - return await get_search_space_llm_instance(session, search_space_id, role) - - -# Legacy aliases for backward compatibility -async def get_long_context_llm( - session: AsyncSession, search_space_id: int -) -> ChatLiteLLM | None: - """Deprecated: Use get_document_summary_llm instead.""" - return await get_document_summary_llm(session, search_space_id) - - -async def get_fast_llm( - session: AsyncSession, search_space_id: int -) -> ChatLiteLLM | None: - """Deprecated: Use get_agent_llm instead.""" - return await get_agent_llm(session, search_space_id) - - -async def get_strategic_llm( - session: AsyncSession, search_space_id: int -) -> ChatLiteLLM | None: - """Deprecated: Use get_document_summary_llm instead.""" - return await get_document_summary_llm(session, search_space_id) - - -# User-based legacy aliases (LLM preferences are now per-search-space, not per-user) +# Backward-compatible alias (LLM preferences are now per-search-space, not per-user) async def get_user_long_context_llm( session: AsyncSession, user_id: str, search_space_id: int ) -> ChatLiteLLM | None: @@ -384,23 +351,3 @@ async def get_user_long_context_llm( The user_id parameter is ignored as LLM preferences are now per-search-space. """ return await get_document_summary_llm(session, search_space_id) - - -async def get_user_fast_llm( - session: AsyncSession, user_id: str, search_space_id: int -) -> ChatLiteLLM | None: - """ - Deprecated: Use get_agent_llm instead. - The user_id parameter is ignored as LLM preferences are now per-search-space. - """ - return await get_agent_llm(session, search_space_id) - - -async def get_user_strategic_llm( - session: AsyncSession, user_id: str, search_space_id: int -) -> ChatLiteLLM | None: - """ - Deprecated: Use get_document_summary_llm instead. - The user_id parameter is ignored as LLM preferences are now per-search-space. - """ - return await get_document_summary_llm(session, search_space_id) diff --git a/surfsense_backend/app/services/query_service.py b/surfsense_backend/app/services/query_service.py deleted file mode 100644 index 863ff58a4..000000000 --- a/surfsense_backend/app/services/query_service.py +++ /dev/null @@ -1,114 +0,0 @@ -import datetime -from typing import Any - -from langchain_core.messages import AIMessage, HumanMessage, SystemMessage -from sqlalchemy.ext.asyncio import AsyncSession - -from app.services.llm_service import get_document_summary_llm - - -class QueryService: - """ - Service for query-related operations, including reformulation and processing. - """ - - @staticmethod - async def reformulate_query_with_chat_history( - user_query: str, - session: AsyncSession, - search_space_id: int, - chat_history_str: str | None = None, - ) -> str: - """ - Reformulate the user query using the search space's document summary LLM to make it more - effective for information retrieval and research purposes. - - Args: - user_query: The original user query - session: Database session for accessing LLM configs - search_space_id: Search Space ID to get LLM preferences - chat_history_str: Optional chat history string - - Returns: - str: The reformulated query - """ - if not user_query or not user_query.strip(): - return user_query - - try: - # Get the search space's document summary LLM instance - llm = await get_document_summary_llm(session, search_space_id) - if not llm: - print( - f"Warning: No document summary LLM configured for search space {search_space_id}. Using original query." - ) - return user_query - - # Create system message with instructions - system_message = SystemMessage( - content=f""" - Today's date: {datetime.datetime.now().strftime("%Y-%m-%d")} - You are a highly skilled AI assistant specializing in query optimization for advanced research. - Your primary objective is to transform a user's initial query into a highly effective search query. - This reformulated query will be used to retrieve information from diverse data sources. - - **Chat History Context:** - {chat_history_str if chat_history_str else "No prior conversation history is available."} - If chat history is provided, analyze it to understand the user's evolving information needs and the broader context of their request. Use this understanding to refine the current query, ensuring it builds upon or clarifies previous interactions. - - **Query Reformulation Guidelines:** - Your reformulated query should: - 1. **Enhance Specificity and Detail:** Add precision to narrow the search focus effectively, making the query less ambiguous and more targeted. - 2. **Resolve Ambiguities:** Identify and clarify vague terms or phrases. If a term has multiple meanings, orient the query towards the most likely one given the context. - 3. **Expand Key Concepts:** Incorporate relevant synonyms, related terms, and alternative phrasings for core concepts. This helps capture a wider range of relevant documents. - 4. **Deconstruct Complex Questions:** If the original query is multifaceted, break it down into its core searchable components or rephrase it to address each aspect clearly. The final output must still be a single, coherent query string. - 5. **Optimize for Comprehensiveness:** Ensure the query is structured to uncover all essential facets of the original request, aiming for thorough information retrieval suitable for research. - 6. **Maintain User Intent:** The reformulated query must stay true to the original intent of the user's query. Do not introduce new topics or shift the focus significantly. - - **Crucial Constraints:** - * **Conciseness and Effectiveness:** While aiming for comprehensiveness, the reformulated query MUST be as concise as possible. Eliminate all unnecessary verbosity. Focus on essential keywords, entities, and concepts that directly contribute to effective retrieval. - * **Single, Direct Output:** Return ONLY the reformulated query itself. Do NOT include any explanations, introductory phrases (e.g., "Reformulated query:", "Here is the optimized query:"), or any other surrounding text or markdown formatting. - - Your output should be a single, optimized query string, ready for immediate use in a search system. - """ - ) - - # Create human message with the user query - human_message = HumanMessage( - content=f"Reformulate this query for better research results: {user_query}" - ) - - # Get the response from the LLM - response = await llm.agenerate(messages=[[system_message, human_message]]) - - # Extract the reformulated query from the response - reformulated_query = response.generations[0][0].text.strip() - - # Return the original query if the reformulation is empty - if not reformulated_query: - return user_query - - return reformulated_query - - except Exception as e: - # Log the error and return the original query - print(f"Error reformulating query: {e}") - return user_query - - @staticmethod - async def langchain_chat_history_to_str(chat_history: list[Any]) -> str: - """ - Convert a list of chat history messages to a string. - """ - chat_history_str = "\n" - - for chat_message in chat_history: - if isinstance(chat_message, HumanMessage): - chat_history_str += f"{chat_message.content}\n" - elif isinstance(chat_message, AIMessage): - chat_history_str += f"{chat_message.content}\n" - elif isinstance(chat_message, SystemMessage): - chat_history_str += f"{chat_message.content}\n" - - chat_history_str += "" - return chat_history_str diff --git a/surfsense_backend/app/utils/document_converters.py b/surfsense_backend/app/utils/document_converters.py index 9883a74ed..279b1dbf6 100644 --- a/surfsense_backend/app/utils/document_converters.py +++ b/surfsense_backend/app/utils/document_converters.py @@ -222,88 +222,6 @@ async def convert_document_to_markdown(elements): return "".join(markdown_parts) -def convert_chunks_to_langchain_documents(chunks): - """ - Convert chunks from hybrid search results to LangChain Document objects. - - Args: - chunks: List of chunk dictionaries from hybrid search results - - Returns: - List of LangChain Document objects - """ - try: - from langchain_core.documents import Document as LangChainDocument - except ImportError: - raise ImportError( - "LangChain is not installed. Please install it with `pip install langchain langchain-core`" - ) from None - - langchain_docs = [] - - for chunk in chunks: - # Extract content from the chunk - content = chunk.get("content", "") - - # Create metadata dictionary - metadata = { - "chunk_id": chunk.get("chunk_id"), - "score": chunk.get("score"), - "rank": chunk.get("rank") if "rank" in chunk else None, - } - - # Add document information to metadata - if "document" in chunk: - doc = chunk["document"] - metadata.update( - { - "document_id": doc.get("id"), - "document_title": doc.get("title"), - "document_type": doc.get("document_type"), - } - ) - - # Add document metadata if available - if "metadata" in doc: - # Prefix document metadata keys to avoid conflicts - doc_metadata = { - f"doc_meta_{k}": v for k, v in doc.get("metadata", {}).items() - } - metadata.update(doc_metadata) - - # Add source URL if available in metadata - if "url" in doc.get("metadata", {}): - metadata["source"] = doc["metadata"]["url"] - elif "sourceURL" in doc.get("metadata", {}): - metadata["source"] = doc["metadata"]["sourceURL"] - - # Ensure source_id is set for citation purposes - # Use document_id as the source_id if available - if "document_id" in metadata: - metadata["source_id"] = metadata["document_id"] - - # Update content for citation mode - format as XML with explicit source_id - new_content = f""" - - - {metadata.get("source_id", metadata.get("document_id", "unknown"))} - - - - {content} - - - - """ - - # Create LangChain Document - langchain_doc = LangChainDocument(page_content=new_content, metadata=metadata) - - langchain_docs.append(langchain_doc) - - return langchain_docs - - def generate_content_hash(content: str, search_space_id: int) -> str: """Generate SHA-256 hash for the given content combined with search space ID.""" combined_data = f"{search_space_id}:{content}" diff --git a/surfsense_web/components/copy-button.tsx b/surfsense_web/components/copy-button.tsx deleted file mode 100644 index c1a752997..000000000 --- a/surfsense_web/components/copy-button.tsx +++ /dev/null @@ -1,38 +0,0 @@ -"use client"; -import { Copy, CopyCheck } from "lucide-react"; -import type { RefObject } from "react"; -import { useEffect, useRef, useState } from "react"; -import { Button } from "./ui/button"; - -export default function CopyButton({ ref }: { ref: RefObject }) { - const [copy, setCopy] = useState(false); - const timeoutRef = useRef(null); - - useEffect(() => { - return () => { - if (timeoutRef.current) { - clearTimeout(timeoutRef.current); - } - }; - }, []); - - const handleClick = () => { - if (ref.current) { - const text = ref.current.innerText; - navigator.clipboard.writeText(text); - - setCopy(true); - timeoutRef.current = setTimeout(() => { - setCopy(false); - }, 2000); - } - }; - - return ( -
    - -
    - ); -} diff --git a/surfsense_web/components/editConnector/EditConnectorLoadingSkeleton.tsx b/surfsense_web/components/editConnector/EditConnectorLoadingSkeleton.tsx deleted file mode 100644 index 4b9965632..000000000 --- a/surfsense_web/components/editConnector/EditConnectorLoadingSkeleton.tsx +++ /dev/null @@ -1,22 +0,0 @@ -"use client"; - -import { Card, CardContent, CardHeader } from "@/components/ui/card"; -import { Skeleton } from "@/components/ui/skeleton"; - -export function EditConnectorLoadingSkeleton() { - return ( -
    - - - - - - - - - - - -
    - ); -} diff --git a/surfsense_web/components/editConnector/EditConnectorNameForm.tsx b/surfsense_web/components/editConnector/EditConnectorNameForm.tsx deleted file mode 100644 index 0dae174db..000000000 --- a/surfsense_web/components/editConnector/EditConnectorNameForm.tsx +++ /dev/null @@ -1,28 +0,0 @@ -"use client"; - -import type { Control } from "react-hook-form"; -import { FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; - -// Assuming EditConnectorFormValues is defined elsewhere or passed as generic -interface EditConnectorNameFormProps { - control: Control; // Use Control if type is available -} - -export function EditConnectorNameForm({ control }: EditConnectorNameFormProps) { - return ( - ( - - Connector Name - - - - - - )} - /> - ); -} diff --git a/surfsense_web/components/editConnector/EditGitHubConnectorConfig.tsx b/surfsense_web/components/editConnector/EditGitHubConnectorConfig.tsx deleted file mode 100644 index aa3eb1404..000000000 --- a/surfsense_web/components/editConnector/EditGitHubConnectorConfig.tsx +++ /dev/null @@ -1,189 +0,0 @@ -import { CircleAlert, Edit, KeyRound, Loader2 } from "lucide-react"; -import type React from "react"; -import type { UseFormReturn } from "react-hook-form"; -import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; -import { Button } from "@/components/ui/button"; -import { Checkbox } from "@/components/ui/checkbox"; -import { - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -import { Skeleton } from "@/components/ui/skeleton"; - -// Types needed from parent -interface GithubRepo { - id: number; - name: string; - full_name: string; - private: boolean; - url: string; - description: string | null; - last_updated: string | null; -} -type GithubPatFormValues = { github_pat: string }; -type EditMode = "viewing" | "editing_repos"; - -interface EditGitHubConnectorConfigProps { - // State from parent - editMode: EditMode; - originalPat: string; - currentSelectedRepos: string[]; - fetchedRepos: GithubRepo[] | null; - newSelectedRepos: string[]; - isFetchingRepos: boolean; - // Forms from parent - patForm: UseFormReturn; - // Handlers from parent - setEditMode: (mode: EditMode) => void; - handleFetchRepositories: (values: GithubPatFormValues) => Promise; - handleRepoSelectionChange: (repoFullName: string, checked: boolean) => void; - setNewSelectedRepos: React.Dispatch>; - setFetchedRepos: React.Dispatch>; -} - -export function EditGitHubConnectorConfig({ - editMode, - originalPat, - currentSelectedRepos, - fetchedRepos, - newSelectedRepos, - isFetchingRepos, - patForm, - setEditMode, - handleFetchRepositories, - handleRepoSelectionChange, - setNewSelectedRepos, - setFetchedRepos, -}: EditGitHubConnectorConfigProps) { - return ( -
    -

    Repository Selection & Access

    - - {/* Viewing Mode */} - {editMode === "viewing" && ( -
    - Currently Indexed Repositories: - {currentSelectedRepos.length > 0 ? ( -
      - {currentSelectedRepos.map((repo) => ( -
    • {repo}
    • - ))} -
    - ) : ( -

    (No repositories currently selected)

    - )} - - - To change repo selections or update the PAT, click above. - -
    - )} - - {/* Editing Mode */} - {editMode === "editing_repos" && ( -
    - {/* PAT Input */} -
    - ( - - - GitHub PAT - - - - - - Enter PAT to fetch/update repos or if you need to update the stored token. - - - - )} - /> - -
    - - {/* Repo List */} - {isFetchingRepos && } - {!isFetchingRepos && - fetchedRepos !== null && - (fetchedRepos.length === 0 ? ( - - - No Repositories Found - Check PAT & permissions. - - ) : ( -
    - - Select Repositories to Index ({newSelectedRepos.length} selected): - -
    - {fetchedRepos.map((repo) => ( -
    - - handleRepoSelectionChange(repo.full_name, !!checked) - } - /> - -
    - ))} -
    -
    - ))} - -
    - )} -
    - ); -} diff --git a/surfsense_web/components/editConnector/EditSimpleTokenForm.tsx b/surfsense_web/components/editConnector/EditSimpleTokenForm.tsx deleted file mode 100644 index 4ad654045..000000000 --- a/surfsense_web/components/editConnector/EditSimpleTokenForm.tsx +++ /dev/null @@ -1,49 +0,0 @@ -"use client"; - -import { KeyRound } from "lucide-react"; -import type { Control } from "react-hook-form"; -import { - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; - -// Assuming EditConnectorFormValues is defined elsewhere or passed as generic -interface EditSimpleTokenFormProps { - control: Control; - fieldName: string; // e.g., "SLACK_BOT_TOKEN" - fieldLabel: string; // e.g., "Slack Bot Token" - fieldDescription: string; - placeholder?: string; -} - -export function EditSimpleTokenForm({ - control, - fieldName, - fieldLabel, - fieldDescription, - placeholder, -}: EditSimpleTokenFormProps) { - return ( - ( - - - {fieldLabel} - - - - - {fieldDescription} - - - )} - /> - ); -} diff --git a/surfsense_web/components/editConnector/types.ts b/surfsense_web/components/editConnector/types.ts deleted file mode 100644 index 43fab23e0..000000000 --- a/surfsense_web/components/editConnector/types.ts +++ /dev/null @@ -1,59 +0,0 @@ -import * as z from "zod"; - -// Types -export interface GithubRepo { - id: number; - name: string; - full_name: string; - private: boolean; - url: string; - description: string | null; - last_updated: string | null; -} - -export type EditMode = "viewing" | "editing_repos"; - -// Schemas -export const githubPatSchema = z.object({ - github_pat: z - .string() - .min(20, { message: "GitHub Personal Access Token seems too short." }) - .refine((pat) => pat.startsWith("ghp_") || pat.startsWith("github_pat_"), { - message: "GitHub PAT should start with 'ghp_' or 'github_pat_'", - }), -}); -export type GithubPatFormValues = z.infer; - -export const editConnectorSchema = z.object({ - name: z.string().min(3, { message: "Connector name must be at least 3 characters." }), - SLACK_BOT_TOKEN: z.string().optional(), - NOTION_INTEGRATION_TOKEN: z.string().optional(), - TAVILY_API_KEY: z.string().optional(), - SEARXNG_HOST: z.string().optional(), - SEARXNG_API_KEY: z.string().optional(), - SEARXNG_ENGINES: z.string().optional(), - SEARXNG_CATEGORIES: z.string().optional(), - SEARXNG_LANGUAGE: z.string().optional(), - SEARXNG_SAFESEARCH: z.string().optional(), - SEARXNG_VERIFY_SSL: z.string().optional(), - LINKUP_API_KEY: z.string().optional(), - DISCORD_BOT_TOKEN: z.string().optional(), - CONFLUENCE_BASE_URL: z.string().optional(), - CONFLUENCE_EMAIL: z.string().optional(), - CONFLUENCE_API_TOKEN: z.string().optional(), - BOOKSTACK_BASE_URL: z.string().optional(), - BOOKSTACK_TOKEN_ID: z.string().optional(), - BOOKSTACK_TOKEN_SECRET: z.string().optional(), - JIRA_BASE_URL: z.string().optional(), - JIRA_EMAIL: z.string().optional(), - JIRA_API_TOKEN: z.string().optional(), - GOOGLE_CALENDAR_CLIENT_ID: z.string().optional(), - GOOGLE_CALENDAR_CLIENT_SECRET: z.string().optional(), - GOOGLE_CALENDAR_REFRESH_TOKEN: z.string().optional(), - GOOGLE_CALENDAR_CALENDAR_IDS: z.string().optional(), - LUMA_API_KEY: z.string().optional(), - ELASTICSEARCH_API_KEY: z.string().optional(), - FIRECRAWL_API_KEY: z.string().optional(), - INITIAL_URLS: z.string().optional(), -}); -export type EditConnectorFormValues = z.infer; diff --git a/surfsense_web/components/inference-params-editor.tsx b/surfsense_web/components/inference-params-editor.tsx index b29275611..3f6dddbf4 100644 --- a/surfsense_web/components/inference-params-editor.tsx +++ b/surfsense_web/components/inference-params-editor.tsx @@ -151,3 +151,4 @@ export default function InferenceParamsEditor({ params, setParams }: InferencePa
    ); } + diff --git a/surfsense_web/components/settings/llm-role-manager.tsx b/surfsense_web/components/settings/llm-role-manager.tsx index 1bf7a3629..ba4c4970c 100644 --- a/surfsense_web/components/settings/llm-role-manager.tsx +++ b/surfsense_web/components/settings/llm-role-manager.tsx @@ -45,7 +45,7 @@ const ROLE_DESCRIPTIONS = { document_summary: { icon: FileText, title: "Document Summary LLM", - description: "Handles document summarization, long context analysis, and query reformulation", + description: "Handles document summarization", color: "bg-purple-100 text-purple-800 border-purple-200", examples: "Document analysis, podcasts, research synthesis", characteristics: ["Large context window", "Deep reasoning", "Summarization"], @@ -74,7 +74,6 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) { data: preferences = {}, isFetching: preferencesLoading, error: preferencesError, - refetch: refreshPreferences, } = useAtomValue(llmPreferencesAtom); const { mutateAsync: updatePreferences } = useAtomValue(updateLLMPreferencesMutationAtom); @@ -187,19 +186,6 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) { Refresh Configs Configs -
    diff --git a/surfsense_web/components/sources/types.ts b/surfsense_web/components/sources/types.ts deleted file mode 100644 index 230af7503..000000000 --- a/surfsense_web/components/sources/types.ts +++ /dev/null @@ -1,13 +0,0 @@ -export interface Connector { - id: string; - title: string; - description: string; - icon: React.ReactNode; - status: "available" | "coming-soon" | "connected"; -} - -export interface ConnectorCategory { - id: string; - title: string; - connectors: Connector[]; -} diff --git a/surfsense_web/components/tool-ui/shared/action-buttons.tsx b/surfsense_web/components/tool-ui/shared/action-buttons.tsx deleted file mode 100644 index 4ed280559..000000000 --- a/surfsense_web/components/tool-ui/shared/action-buttons.tsx +++ /dev/null @@ -1,41 +0,0 @@ -"use client"; - -import type { FC } from "react"; -import { Button } from "@/components/ui/button"; -import type { Action, ActionsConfig } from "./schema"; - -interface ActionButtonsProps { - actions?: Action[] | ActionsConfig; - onAction?: (actionId: string) => void; - disabled?: boolean; -} - -export const ActionButtons: FC = ({ actions, onAction, disabled }) => { - if (!actions) return null; - - // Normalize actions to array format - const actionArray: Action[] = Array.isArray(actions) - ? actions - : ([ - actions.confirm && { ...actions.confirm, id: "confirm" }, - actions.cancel && { ...actions.cancel, id: "cancel" }, - ].filter(Boolean) as Action[]); - - if (actionArray.length === 0) return null; - - return ( -
    - {actionArray.map((action) => ( - - ))} -
    - ); -}; diff --git a/surfsense_web/components/tool-ui/shared/index.ts b/surfsense_web/components/tool-ui/shared/index.ts deleted file mode 100644 index 23f5a27dd..000000000 --- a/surfsense_web/components/tool-ui/shared/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./action-buttons"; -export * from "./schema"; diff --git a/surfsense_web/components/tool-ui/shared/schema.ts b/surfsense_web/components/tool-ui/shared/schema.ts deleted file mode 100644 index 8076a8e45..000000000 --- a/surfsense_web/components/tool-ui/shared/schema.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { z } from "zod"; - -/** - * Shared action schema for tool UI components - */ -export const ActionSchema = z.object({ - id: z.string(), - label: z.string(), - variant: z.enum(["default", "secondary", "destructive", "outline", "ghost", "link"]).optional(), - disabled: z.boolean().optional(), -}); - -export type Action = z.infer; - -/** - * Actions configuration schema - */ -export const ActionsConfigSchema = z.object({ - confirm: ActionSchema.optional(), - cancel: ActionSchema.optional(), -}); - -export type ActionsConfig = z.infer; diff --git a/surfsense_web/hooks/use-chat.ts b/surfsense_web/hooks/use-chat.ts deleted file mode 100644 index c31097e11..000000000 --- a/surfsense_web/hooks/use-chat.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { useEffect, useState } from "react"; -import type { ResearchMode } from "@/components/chat"; -import type { Document } from "@/contracts/types/document.types"; -import { getBearerToken } from "@/lib/auth-utils"; - -interface UseChatStateProps { - search_space_id: string; - chat_id?: string; -} - -export function useChatState({ chat_id }: UseChatStateProps) { - const [token, setToken] = useState(null); - const [isLoading, setIsLoading] = useState(false); - const [currentChatId, setCurrentChatId] = useState(chat_id || null); - - // Chat configuration state - const [researchMode, setResearchMode] = useState("QNA"); - const [selectedConnectors, setSelectedConnectors] = useState([]); - const [selectedDocuments, setSelectedDocuments] = useState([]); - const [topK, setTopK] = useState(5); - - useEffect(() => { - const bearerToken = getBearerToken(); - setToken(bearerToken); - }, []); - - return { - token, - setToken, - isLoading, - setIsLoading, - currentChatId, - setCurrentChatId, - researchMode, - setResearchMode, - selectedConnectors, - setSelectedConnectors, - selectedDocuments, - setSelectedDocuments, - topK, - setTopK, - }; -} diff --git a/surfsense_web/hooks/use-connector-edit-page.ts b/surfsense_web/hooks/use-connector-edit-page.ts deleted file mode 100644 index a1a3c88f4..000000000 --- a/surfsense_web/hooks/use-connector-edit-page.ts +++ /dev/null @@ -1,680 +0,0 @@ -import { zodResolver } from "@hookform/resolvers/zod"; -import { useAtomValue } from "jotai"; -import { useRouter } from "next/navigation"; -import { useCallback, useEffect, useState } from "react"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; -import { updateConnectorMutationAtom } from "@/atoms/connectors/connector-mutation.atoms"; -import { connectorsAtom } from "@/atoms/connectors/connector-query.atoms"; -import { - type EditConnectorFormValues, - type EditMode, - editConnectorSchema, - type GithubPatFormValues, - type GithubRepo, - githubPatSchema, -} from "@/components/editConnector/types"; -import type { EnumConnectorName } from "@/contracts/enums/connector"; -import type { UpdateConnectorResponse } from "@/contracts/types/connector.types"; -import type { SearchSourceConnector } from "@/hooks/use-search-source-connectors"; -import { authenticatedFetch } from "@/lib/auth-utils"; - -const normalizeListInput = (value: unknown): string[] => { - if (Array.isArray(value)) { - return value.map((item) => String(item).trim()).filter((item) => item.length > 0); - } - if (typeof value === "string") { - return value - .split(",") - .map((item) => item.trim()) - .filter((item) => item.length > 0); - } - return []; -}; - -const arraysEqual = (a: string[], b: string[]): boolean => { - if (a.length !== b.length) return false; - return a.every((value, index) => value === b[index]); -}; - -const normalizeBoolean = (value: unknown): boolean | null => { - if (typeof value === "boolean") return value; - if (typeof value === "string") { - const lowered = value.trim().toLowerCase(); - if (["true", "1", "yes", "on"].includes(lowered)) return true; - if (["false", "0", "no", "off"].includes(lowered)) return false; - } - if (typeof value === "number") { - if (value === 1) return true; - if (value === 0) return false; - } - return null; -}; - -export function useConnectorEditPage(connectorId: number, searchSpaceId: string) { - const router = useRouter(); - const { data: connectors = [], isLoading: connectorsLoading } = useAtomValue(connectorsAtom); - const { mutateAsync: updateConnector } = useAtomValue(updateConnectorMutationAtom); - - // State managed by the hook - const [connector, setConnector] = useState(null); - const [originalConfig, setOriginalConfig] = useState | null>(null); - const [isSaving, setIsSaving] = useState(false); - const [currentSelectedRepos, setCurrentSelectedRepos] = useState([]); - const [originalPat, setOriginalPat] = useState(""); - const [editMode, setEditMode] = useState("viewing"); - const [fetchedRepos, setFetchedRepos] = useState(null); - const [newSelectedRepos, setNewSelectedRepos] = useState([]); - const [isFetchingRepos, setIsFetchingRepos] = useState(false); - - // Forms managed by the hook - const patForm = useForm({ - resolver: zodResolver(githubPatSchema), - defaultValues: { github_pat: "" }, - }); - const editForm = useForm({ - resolver: zodResolver(editConnectorSchema), - defaultValues: { - name: "", - SLACK_BOT_TOKEN: "", - NOTION_INTEGRATION_TOKEN: "", - TAVILY_API_KEY: "", - SEARXNG_HOST: "", - SEARXNG_API_KEY: "", - SEARXNG_ENGINES: "", - SEARXNG_CATEGORIES: "", - SEARXNG_LANGUAGE: "", - SEARXNG_SAFESEARCH: "", - SEARXNG_VERIFY_SSL: "", - DISCORD_BOT_TOKEN: "", - CONFLUENCE_BASE_URL: "", - CONFLUENCE_EMAIL: "", - CONFLUENCE_API_TOKEN: "", - BOOKSTACK_BASE_URL: "", - BOOKSTACK_TOKEN_ID: "", - BOOKSTACK_TOKEN_SECRET: "", - JIRA_BASE_URL: "", - JIRA_EMAIL: "", - JIRA_API_TOKEN: "", - LUMA_API_KEY: "", - ELASTICSEARCH_API_KEY: "", - FIRECRAWL_API_KEY: "", - INITIAL_URLS: "", - }, - }); - - // Effect to load initial data - useEffect(() => { - if (!connectorsLoading && connectors.length > 0 && !connector) { - const currentConnector = connectors.find((c) => c.id === connectorId); - if (currentConnector) { - setConnector(currentConnector); - const config = currentConnector.config || {}; - setOriginalConfig(config); - editForm.reset({ - name: currentConnector.name, - SLACK_BOT_TOKEN: config.SLACK_BOT_TOKEN || "", - NOTION_INTEGRATION_TOKEN: config.NOTION_INTEGRATION_TOKEN || "", - TAVILY_API_KEY: config.TAVILY_API_KEY || "", - SEARXNG_HOST: config.SEARXNG_HOST || "", - SEARXNG_API_KEY: config.SEARXNG_API_KEY || "", - SEARXNG_ENGINES: Array.isArray(config.SEARXNG_ENGINES) - ? config.SEARXNG_ENGINES.join(", ") - : config.SEARXNG_ENGINES || "", - SEARXNG_CATEGORIES: Array.isArray(config.SEARXNG_CATEGORIES) - ? config.SEARXNG_CATEGORIES.join(", ") - : config.SEARXNG_CATEGORIES || "", - SEARXNG_LANGUAGE: config.SEARXNG_LANGUAGE || "", - SEARXNG_SAFESEARCH: - config.SEARXNG_SAFESEARCH !== undefined && config.SEARXNG_SAFESEARCH !== null - ? String(config.SEARXNG_SAFESEARCH) - : "", - SEARXNG_VERIFY_SSL: - config.SEARXNG_VERIFY_SSL !== undefined && config.SEARXNG_VERIFY_SSL !== null - ? String(config.SEARXNG_VERIFY_SSL) - : "", - LINKUP_API_KEY: config.LINKUP_API_KEY || "", - DISCORD_BOT_TOKEN: config.DISCORD_BOT_TOKEN || "", - CONFLUENCE_BASE_URL: config.CONFLUENCE_BASE_URL || "", - CONFLUENCE_EMAIL: config.CONFLUENCE_EMAIL || "", - CONFLUENCE_API_TOKEN: config.CONFLUENCE_API_TOKEN || "", - BOOKSTACK_BASE_URL: config.BOOKSTACK_BASE_URL || "", - BOOKSTACK_TOKEN_ID: config.BOOKSTACK_TOKEN_ID || "", - BOOKSTACK_TOKEN_SECRET: config.BOOKSTACK_TOKEN_SECRET || "", - JIRA_BASE_URL: config.JIRA_BASE_URL || "", - JIRA_EMAIL: config.JIRA_EMAIL || "", - JIRA_API_TOKEN: config.JIRA_API_TOKEN || "", - LUMA_API_KEY: config.LUMA_API_KEY || "", - ELASTICSEARCH_API_KEY: config.ELASTICSEARCH_API_KEY || "", - FIRECRAWL_API_KEY: config.FIRECRAWL_API_KEY || "", - INITIAL_URLS: config.INITIAL_URLS || "", - }); - if (currentConnector.connector_type === "GITHUB_CONNECTOR") { - const savedRepos = config.repo_full_names || []; - const savedPat = config.GITHUB_PAT || ""; - setCurrentSelectedRepos(savedRepos); - setNewSelectedRepos(savedRepos); - setOriginalPat(savedPat); - patForm.reset({ github_pat: savedPat }); - setEditMode("viewing"); - } - } else { - toast.error("Connector not found."); - router.push(`/dashboard/${searchSpaceId}`); - } - } - }, [ - connectorId, - connectors, - connectorsLoading, - router, - searchSpaceId, - connector, - editForm.reset, - patForm.reset, - // Note: editForm and patForm are intentionally excluded from dependencies - // to prevent infinite loops. They are stable form objects from react-hook-form. - ]); - - // Handlers managed by the hook - const handleFetchRepositories = useCallback( - async (values: GithubPatFormValues) => { - setIsFetchingRepos(true); - setFetchedRepos(null); - try { - const response = await authenticatedFetch( - `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/github/repositories`, - { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ github_pat: values.github_pat }), - } - ); - if (!response.ok) { - const err = await response.json(); - throw new Error(err.detail || "Fetch failed"); - } - const data: GithubRepo[] = await response.json(); - setFetchedRepos(data); - setNewSelectedRepos(currentSelectedRepos); - toast.success(`Found ${data.length} repos.`); - } catch (error) { - console.error("Error fetching GitHub repositories:", error); - toast.error(error instanceof Error ? error.message : "Failed to fetch repositories."); - } finally { - setIsFetchingRepos(false); - } - }, - [currentSelectedRepos] - ); // Added dependency - - const handleRepoSelectionChange = useCallback((repoFullName: string, checked: boolean) => { - setNewSelectedRepos((prev) => - checked ? [...prev, repoFullName] : prev.filter((name) => name !== repoFullName) - ); - }, []); - - const handleSaveChanges = useCallback( - async (formData: EditConnectorFormValues) => { - if (!connector || !originalConfig) return; - setIsSaving(true); - const updatePayload: Partial = {}; - let configChanged = false; - let newConfig: Record | null = null; - - if (formData.name !== connector.name) { - updatePayload.name = formData.name; - } - - switch (connector.connector_type) { - case "GITHUB_CONNECTOR": { - const currentPatInForm = patForm.getValues("github_pat"); - const patChanged = currentPatInForm !== originalPat; - const initialRepoSet = new Set(currentSelectedRepos); - const newRepoSet = new Set(newSelectedRepos); - const reposChanged = - initialRepoSet.size !== newRepoSet.size || - ![...initialRepoSet].every((repo) => newRepoSet.has(repo)); - if ( - patChanged || - (editMode === "editing_repos" && reposChanged && fetchedRepos !== null) - ) { - if ( - !currentPatInForm || - !(currentPatInForm.startsWith("ghp_") || currentPatInForm.startsWith("github_pat_")) - ) { - toast.error("Invalid GitHub PAT format. Cannot save."); - setIsSaving(false); - return; - } - newConfig = { - GITHUB_PAT: currentPatInForm, - repo_full_names: newSelectedRepos, - }; - if (reposChanged && newSelectedRepos.length === 0) { - toast.warning("Warning: No repositories selected."); - } - } - break; - } - case "SLACK_CONNECTOR": - if (formData.SLACK_BOT_TOKEN !== originalConfig.SLACK_BOT_TOKEN) { - if (!formData.SLACK_BOT_TOKEN) { - toast.error("Slack Token empty."); - setIsSaving(false); - return; - } - newConfig = { SLACK_BOT_TOKEN: formData.SLACK_BOT_TOKEN }; - } - break; - case "NOTION_CONNECTOR": - if (formData.NOTION_INTEGRATION_TOKEN !== originalConfig.NOTION_INTEGRATION_TOKEN) { - if (!formData.NOTION_INTEGRATION_TOKEN) { - toast.error("Notion Token empty."); - setIsSaving(false); - return; - } - newConfig = { - NOTION_INTEGRATION_TOKEN: formData.NOTION_INTEGRATION_TOKEN, - }; - } - break; - case "TAVILY_API": - if (formData.TAVILY_API_KEY !== originalConfig.TAVILY_API_KEY) { - if (!formData.TAVILY_API_KEY) { - toast.error("Tavily Key empty."); - setIsSaving(false); - return; - } - newConfig = { TAVILY_API_KEY: formData.TAVILY_API_KEY }; - } - break; - case "SEARXNG_API": { - const host = (formData.SEARXNG_HOST || "").trim(); - if (!host) { - toast.error("SearxNG host is required."); - setIsSaving(false); - return; - } - - const candidateConfig: Record = { SEARXNG_HOST: host }; - const originalHost = - typeof originalConfig.SEARXNG_HOST === "string" ? originalConfig.SEARXNG_HOST : ""; - let hasChanges = host !== originalHost.trim(); - - const apiKey = (formData.SEARXNG_API_KEY || "").trim(); - const originalApiKey = - typeof originalConfig.SEARXNG_API_KEY === "string" - ? originalConfig.SEARXNG_API_KEY - : ""; - const originalApiKeyTrimmed = originalApiKey.trim(); - if (apiKey !== originalApiKeyTrimmed) { - candidateConfig.SEARXNG_API_KEY = apiKey || null; - hasChanges = true; - } - - const newEngines = normalizeListInput(formData.SEARXNG_ENGINES || ""); - const originalEngines = normalizeListInput(originalConfig.SEARXNG_ENGINES); - if (!arraysEqual(newEngines, originalEngines)) { - candidateConfig.SEARXNG_ENGINES = newEngines; - hasChanges = true; - } - - const newCategories = normalizeListInput(formData.SEARXNG_CATEGORIES || ""); - const originalCategories = normalizeListInput(originalConfig.SEARXNG_CATEGORIES); - if (!arraysEqual(newCategories, originalCategories)) { - candidateConfig.SEARXNG_CATEGORIES = newCategories; - hasChanges = true; - } - - const language = (formData.SEARXNG_LANGUAGE || "").trim(); - const originalLanguage = - typeof originalConfig.SEARXNG_LANGUAGE === "string" - ? originalConfig.SEARXNG_LANGUAGE - : ""; - const originalLanguageTrimmed = originalLanguage.trim(); - if (language !== originalLanguageTrimmed) { - candidateConfig.SEARXNG_LANGUAGE = language || null; - hasChanges = true; - } - - const safesearchRaw = (formData.SEARXNG_SAFESEARCH || "").trim(); - const originalSafesearch = originalConfig.SEARXNG_SAFESEARCH; - if (safesearchRaw) { - const parsed = Number(safesearchRaw); - if (Number.isNaN(parsed) || !Number.isInteger(parsed) || parsed < 0 || parsed > 2) { - toast.error("SearxNG SafeSearch must be 0, 1, or 2."); - setIsSaving(false); - return; - } - if (parsed !== Number(originalSafesearch)) { - candidateConfig.SEARXNG_SAFESEARCH = parsed; - hasChanges = true; - } - } else if (originalSafesearch !== undefined && originalSafesearch !== null) { - candidateConfig.SEARXNG_SAFESEARCH = null; - hasChanges = true; - } - - const verifyRaw = (formData.SEARXNG_VERIFY_SSL || "").trim().toLowerCase(); - const originalVerifyBool = normalizeBoolean(originalConfig.SEARXNG_VERIFY_SSL); - if (verifyRaw) { - let parsedBool: boolean | null = null; - if (["true", "1", "yes", "on"].includes(verifyRaw)) parsedBool = true; - else if (["false", "0", "no", "off"].includes(verifyRaw)) parsedBool = false; - if (parsedBool === null) { - toast.error("SearxNG SSL verification must be true or false."); - setIsSaving(false); - return; - } - if (parsedBool !== originalVerifyBool) { - candidateConfig.SEARXNG_VERIFY_SSL = parsedBool; - hasChanges = true; - } - } else if (originalVerifyBool !== null) { - candidateConfig.SEARXNG_VERIFY_SSL = null; - hasChanges = true; - } - - if (hasChanges) { - newConfig = candidateConfig; - } - break; - } - - case "LINKUP_API": - if (formData.LINKUP_API_KEY !== originalConfig.LINKUP_API_KEY) { - if (!formData.LINKUP_API_KEY) { - toast.error("Linkup API Key cannot be empty."); - setIsSaving(false); - return; - } - newConfig = { LINKUP_API_KEY: formData.LINKUP_API_KEY }; - } - break; - case "DISCORD_CONNECTOR": - if (formData.DISCORD_BOT_TOKEN !== originalConfig.DISCORD_BOT_TOKEN) { - if (!formData.DISCORD_BOT_TOKEN) { - toast.error("Discord Bot Token cannot be empty."); - setIsSaving(false); - return; - } - newConfig = { DISCORD_BOT_TOKEN: formData.DISCORD_BOT_TOKEN }; - } - break; - case "CONFLUENCE_CONNECTOR": - if ( - formData.CONFLUENCE_BASE_URL !== originalConfig.CONFLUENCE_BASE_URL || - formData.CONFLUENCE_EMAIL !== originalConfig.CONFLUENCE_EMAIL || - formData.CONFLUENCE_API_TOKEN !== originalConfig.CONFLUENCE_API_TOKEN - ) { - if ( - !formData.CONFLUENCE_BASE_URL || - !formData.CONFLUENCE_EMAIL || - !formData.CONFLUENCE_API_TOKEN - ) { - toast.error("All Confluence fields are required."); - setIsSaving(false); - return; - } - newConfig = { - CONFLUENCE_BASE_URL: formData.CONFLUENCE_BASE_URL, - CONFLUENCE_EMAIL: formData.CONFLUENCE_EMAIL, - CONFLUENCE_API_TOKEN: formData.CONFLUENCE_API_TOKEN, - }; - } - break; - case "BOOKSTACK_CONNECTOR": - if ( - formData.BOOKSTACK_BASE_URL !== originalConfig.BOOKSTACK_BASE_URL || - formData.BOOKSTACK_TOKEN_ID !== originalConfig.BOOKSTACK_TOKEN_ID || - formData.BOOKSTACK_TOKEN_SECRET !== originalConfig.BOOKSTACK_TOKEN_SECRET - ) { - if ( - !formData.BOOKSTACK_BASE_URL || - !formData.BOOKSTACK_TOKEN_ID || - !formData.BOOKSTACK_TOKEN_SECRET - ) { - toast.error("All BookStack fields are required."); - setIsSaving(false); - return; - } - newConfig = { - BOOKSTACK_BASE_URL: formData.BOOKSTACK_BASE_URL, - BOOKSTACK_TOKEN_ID: formData.BOOKSTACK_TOKEN_ID, - BOOKSTACK_TOKEN_SECRET: formData.BOOKSTACK_TOKEN_SECRET, - }; - } - break; - case "JIRA_CONNECTOR": { - // Check if this is an OAuth connector (has access_token or _token_encrypted flag) - const isJiraOAuth = !!(originalConfig.access_token || originalConfig._token_encrypted); - - if (isJiraOAuth) { - // OAuth connectors don't allow editing credentials through the form - // Only allow name changes, which are handled separately - break; - } - - // Legacy API token connector - allow editing credentials - if ( - formData.JIRA_BASE_URL !== originalConfig.JIRA_BASE_URL || - formData.JIRA_EMAIL !== originalConfig.JIRA_EMAIL || - formData.JIRA_API_TOKEN !== originalConfig.JIRA_API_TOKEN - ) { - if (!formData.JIRA_BASE_URL || !formData.JIRA_EMAIL || !formData.JIRA_API_TOKEN) { - toast.error("All Jira fields are required."); - setIsSaving(false); - return; - } - newConfig = { - JIRA_BASE_URL: formData.JIRA_BASE_URL, - JIRA_EMAIL: formData.JIRA_EMAIL, - JIRA_API_TOKEN: formData.JIRA_API_TOKEN, - }; - } - break; - } - case "LUMA_CONNECTOR": - if (formData.LUMA_API_KEY !== originalConfig.LUMA_API_KEY) { - if (!formData.LUMA_API_KEY) { - toast.error("Luma API Key cannot be empty."); - setIsSaving(false); - return; - } - newConfig = { LUMA_API_KEY: formData.LUMA_API_KEY }; - } - break; - case "ELASTICSEARCH_CONNECTOR": - if (formData.ELASTICSEARCH_API_KEY !== originalConfig.ELASTICSEARCH_API_KEY) { - if (!formData.ELASTICSEARCH_API_KEY) { - toast.error("Elasticsearch API Key cannot be empty."); - setIsSaving(false); - return; - } - newConfig = { ELASTICSEARCH_API_KEY: formData.ELASTICSEARCH_API_KEY }; - } - break; - case "WEBCRAWLER_CONNECTOR": - if ( - formData.FIRECRAWL_API_KEY !== originalConfig.FIRECRAWL_API_KEY || - formData.INITIAL_URLS !== originalConfig.INITIAL_URLS - ) { - newConfig = {}; - - if (formData.FIRECRAWL_API_KEY?.trim()) { - if (!formData.FIRECRAWL_API_KEY.startsWith("fc-")) { - toast.warning( - "Firecrawl API keys typically start with 'fc-'. Please verify your key." - ); - } - newConfig.FIRECRAWL_API_KEY = formData.FIRECRAWL_API_KEY.trim(); - } else if (originalConfig.FIRECRAWL_API_KEY) { - toast.info( - "Firecrawl API key removed. Web crawler will use AsyncChromiumLoader as fallback." - ); - } - - if (formData.INITIAL_URLS !== undefined) { - if (formData.INITIAL_URLS?.trim()) { - newConfig.INITIAL_URLS = formData.INITIAL_URLS.trim(); - } else if (originalConfig.INITIAL_URLS) { - toast.info("URLs removed from crawler configuration."); - } - } - } - break; - } - - if (newConfig !== null) { - updatePayload.config = newConfig; - configChanged = true; - } - - if (Object.keys(updatePayload).length === 0) { - toast.info("No changes detected."); - setIsSaving(false); - if (connector.connector_type === "GITHUB_CONNECTOR") { - setEditMode("viewing"); - patForm.reset({ github_pat: originalPat }); - } - return; - } - - try { - const updatedConnector = (await updateConnector({ - id: connectorId, - data: { - ...updatePayload, - connector_type: connector.connector_type as EnumConnectorName, - }, - })) as UpdateConnectorResponse; - toast.success("Connector updated!"); - // Use the response from the API which has the full merged config - const newlySavedConfig = updatedConnector.config || originalConfig; - setOriginalConfig(newlySavedConfig); - // Update connector state with the full updated connector from the API - setConnector(updatedConnector); - if (configChanged) { - if (connector.connector_type === "GITHUB_CONNECTOR") { - const savedGitHubConfig = newlySavedConfig as { - GITHUB_PAT?: string; - repo_full_names?: string[]; - }; - setCurrentSelectedRepos(savedGitHubConfig.repo_full_names || []); - setOriginalPat(savedGitHubConfig.GITHUB_PAT || ""); - setNewSelectedRepos(savedGitHubConfig.repo_full_names || []); - patForm.reset({ github_pat: savedGitHubConfig.GITHUB_PAT || "" }); - } else if (connector.connector_type === "SLACK_CONNECTOR") { - editForm.setValue("SLACK_BOT_TOKEN", newlySavedConfig.SLACK_BOT_TOKEN || ""); - } else if (connector.connector_type === "NOTION_CONNECTOR") { - editForm.setValue( - "NOTION_INTEGRATION_TOKEN", - newlySavedConfig.NOTION_INTEGRATION_TOKEN || "" - ); - } else if (connector.connector_type === "TAVILY_API") { - editForm.setValue("TAVILY_API_KEY", newlySavedConfig.TAVILY_API_KEY || ""); - } else if (connector.connector_type === "SEARXNG_API") { - editForm.setValue("SEARXNG_HOST", newlySavedConfig.SEARXNG_HOST || ""); - editForm.setValue("SEARXNG_API_KEY", newlySavedConfig.SEARXNG_API_KEY || ""); - editForm.setValue( - "SEARXNG_ENGINES", - normalizeListInput(newlySavedConfig.SEARXNG_ENGINES).join(", ") - ); - editForm.setValue( - "SEARXNG_CATEGORIES", - normalizeListInput(newlySavedConfig.SEARXNG_CATEGORIES).join(", ") - ); - editForm.setValue("SEARXNG_LANGUAGE", newlySavedConfig.SEARXNG_LANGUAGE || ""); - editForm.setValue( - "SEARXNG_SAFESEARCH", - newlySavedConfig.SEARXNG_SAFESEARCH === null || - newlySavedConfig.SEARXNG_SAFESEARCH === undefined - ? "" - : String(newlySavedConfig.SEARXNG_SAFESEARCH) - ); - const verifyValue = normalizeBoolean(newlySavedConfig.SEARXNG_VERIFY_SSL); - editForm.setValue( - "SEARXNG_VERIFY_SSL", - verifyValue === null ? "" : String(verifyValue) - ); - } else if (connector.connector_type === "LINKUP_API") { - editForm.setValue("LINKUP_API_KEY", newlySavedConfig.LINKUP_API_KEY || ""); - } else if (connector.connector_type === "DISCORD_CONNECTOR") { - editForm.setValue("DISCORD_BOT_TOKEN", newlySavedConfig.DISCORD_BOT_TOKEN || ""); - } else if (connector.connector_type === "CONFLUENCE_CONNECTOR") { - editForm.setValue("CONFLUENCE_BASE_URL", newlySavedConfig.CONFLUENCE_BASE_URL || ""); - editForm.setValue("CONFLUENCE_EMAIL", newlySavedConfig.CONFLUENCE_EMAIL || ""); - editForm.setValue("CONFLUENCE_API_TOKEN", newlySavedConfig.CONFLUENCE_API_TOKEN || ""); - } else if (connector.connector_type === "BOOKSTACK_CONNECTOR") { - editForm.setValue("BOOKSTACK_BASE_URL", newlySavedConfig.BOOKSTACK_BASE_URL || ""); - editForm.setValue("BOOKSTACK_TOKEN_ID", newlySavedConfig.BOOKSTACK_TOKEN_ID || ""); - editForm.setValue( - "BOOKSTACK_TOKEN_SECRET", - newlySavedConfig.BOOKSTACK_TOKEN_SECRET || "" - ); - } else if (connector.connector_type === "JIRA_CONNECTOR") { - editForm.setValue("JIRA_BASE_URL", newlySavedConfig.JIRA_BASE_URL || ""); - editForm.setValue("JIRA_EMAIL", newlySavedConfig.JIRA_EMAIL || ""); - editForm.setValue("JIRA_API_TOKEN", newlySavedConfig.JIRA_API_TOKEN || ""); - } else if (connector.connector_type === "LUMA_CONNECTOR") { - editForm.setValue("LUMA_API_KEY", newlySavedConfig.LUMA_API_KEY || ""); - } else if (connector.connector_type === "ELASTICSEARCH_CONNECTOR") { - editForm.setValue( - "ELASTICSEARCH_API_KEY", - newlySavedConfig.ELASTICSEARCH_API_KEY || "" - ); - } else if (connector.connector_type === "WEBCRAWLER_CONNECTOR") { - editForm.setValue("FIRECRAWL_API_KEY", newlySavedConfig.FIRECRAWL_API_KEY || ""); - editForm.setValue("INITIAL_URLS", newlySavedConfig.INITIAL_URLS || ""); - } - } - if (connector.connector_type === "GITHUB_CONNECTOR") { - setEditMode("viewing"); - setFetchedRepos(null); - } - // Resetting simple form values is handled by useEffect if connector state updates - } catch (error) { - console.error("Error updating connector:", error); - toast.error(error instanceof Error ? error.message : "Failed to update connector."); - } finally { - setIsSaving(false); - } - }, - [ - connector, - originalConfig, - updateConnector, - connectorId, - patForm, - originalPat, - currentSelectedRepos, - newSelectedRepos, - editMode, - fetchedRepos, - editForm, - ] - ); // Added editForm to dependencies - - // Return values needed by the component - return { - connectorsLoading, - connector, - isSaving, - editForm, - patForm, - handleSaveChanges, - // GitHub specific props - editMode, - setEditMode, - originalPat, - currentSelectedRepos, - fetchedRepos, - setFetchedRepos, - newSelectedRepos, - setNewSelectedRepos, - isFetchingRepos, - handleFetchRepositories, - handleRepoSelectionChange, - }; -} diff --git a/surfsense_web/lib/auth-utils.ts b/surfsense_web/lib/auth-utils.ts index c1dc7194b..604843292 100644 --- a/surfsense_web/lib/auth-utils.ts +++ b/surfsense_web/lib/auth-utils.ts @@ -130,44 +130,3 @@ export async function authenticatedFetch( return response; } - -/** - * Type for the result of a fetch operation with built-in error handling - */ -export type FetchResult = - | { success: true; data: T; response: Response } - | { success: false; error: string; status?: number }; - -/** - * Authenticated fetch with JSON response handling - * Returns a result object instead of throwing on non-401 errors - */ -export async function authenticatedFetchJson( - url: string, - options?: RequestInit & { skipAuthRedirect?: boolean } -): Promise> { - try { - const response = await authenticatedFetch(url, options); - - if (!response.ok) { - const errorData = await response.json().catch(() => ({})); - return { - success: false, - error: errorData.detail || `Request failed: ${response.status}`, - status: response.status, - }; - } - - const data = await response.json(); - return { success: true, data, response }; - } catch (err: any) { - // Re-throw if it's the unauthorized redirect - if (err.message?.includes("Unauthorized")) { - throw err; - } - return { - success: false, - error: err.message || "Request failed", - }; - } -} diff --git a/surfsense_web/lib/utils.ts b/surfsense_web/lib/utils.ts index 1e29bb9a4..212ff1259 100644 --- a/surfsense_web/lib/utils.ts +++ b/surfsense_web/lib/utils.ts @@ -1,4 +1,3 @@ -import type { Message } from "@ai-sdk/react"; import { type ClassValue, clsx } from "clsx"; import { twMerge } from "tailwind-merge"; @@ -6,12 +5,6 @@ export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } -export function getChatTitleFromMessages(messages: Message[]) { - const userMessages = messages.filter((msg) => msg.role === "user"); - if (userMessages.length === 0) return "Untitled Chat"; - return userMessages[0].content; -} - export const formatDate = (date: Date): string => { return date.toLocaleDateString("en-US", { year: "numeric", diff --git a/surfsense_web/messages/en.json b/surfsense_web/messages/en.json index fd655be6c..6c64e62ba 100644 --- a/surfsense_web/messages/en.json +++ b/surfsense_web/messages/en.json @@ -265,7 +265,7 @@ "no_documents": "No documents found", "type": "Type", "content_summary": "Content Summary", - "view_full": "View Full Content", + "view_full": "View Summary", "filter_placeholder": "Filter by title...", "rows_per_page": "Rows per page", "refresh": "Refresh", From a6200ee3a224dc7c7fc3a0a1b5c37fee231a7a60 Mon Sep 17 00:00:00 2001 From: "DESKTOP-RTLN3BA\\$punk" Date: Wed, 7 Jan 2026 19:10:53 -0800 Subject: [PATCH 71/75] chore: linting --- .../versions/57_allow_multiple_connectors_per_type.py | 4 ++-- .../versions/58_unique_connector_name_per_space_user.py | 4 ++-- surfsense_web/components/homepage/hero-section.tsx | 7 ++----- surfsense_web/components/inference-params-editor.tsx | 1 - surfsense_web/components/onboarding-tour.tsx | 8 ++++---- surfsense_web/mdx-components.tsx | 4 ++-- 6 files changed, 12 insertions(+), 16 deletions(-) diff --git a/surfsense_backend/alembic/versions/57_allow_multiple_connectors_per_type.py b/surfsense_backend/alembic/versions/57_allow_multiple_connectors_per_type.py index 25558f42e..a1482ee4b 100644 --- a/surfsense_backend/alembic/versions/57_allow_multiple_connectors_per_type.py +++ b/surfsense_backend/alembic/versions/57_allow_multiple_connectors_per_type.py @@ -8,6 +8,8 @@ Create Date: 2026-01-06 12:00:00.000000 from collections.abc import Sequence +from sqlalchemy import text + from alembic import op # revision identifiers, used by Alembic. @@ -16,8 +18,6 @@ down_revision: str | None = "56" branch_labels: str | Sequence[str] | None = None depends_on: str | Sequence[str] | None = None -from sqlalchemy import text - def upgrade() -> None: connection = op.get_bind() diff --git a/surfsense_backend/alembic/versions/58_unique_connector_name_per_space_user.py b/surfsense_backend/alembic/versions/58_unique_connector_name_per_space_user.py index 7c35ab1d8..4dd8d7b70 100644 --- a/surfsense_backend/alembic/versions/58_unique_connector_name_per_space_user.py +++ b/surfsense_backend/alembic/versions/58_unique_connector_name_per_space_user.py @@ -9,6 +9,8 @@ Create Date: 2026-01-06 14:00:00.000000 from collections.abc import Sequence +from sqlalchemy import text + from alembic import op revision: str = "58" @@ -16,8 +18,6 @@ down_revision: str | None = "57" branch_labels: str | Sequence[str] | None = None depends_on: str | Sequence[str] | None = None -from sqlalchemy import text - def upgrade() -> None: connection = op.get_bind() diff --git a/surfsense_web/components/homepage/hero-section.tsx b/surfsense_web/components/homepage/hero-section.tsx index 4b76e1f7b..a9cfdeba2 100644 --- a/surfsense_web/components/homepage/hero-section.tsx +++ b/surfsense_web/components/homepage/hero-section.tsx @@ -4,8 +4,8 @@ import Image from "next/image"; import Link from "next/link"; import React, { useEffect, useRef, useState } from "react"; import Balancer from "react-wrap-balancer"; -import { cn } from "@/lib/utils"; import { trackLoginAttempt } from "@/lib/posthog/events"; +import { cn } from "@/lib/utils"; // Official Google "G" logo with brand colors const GoogleLogo = ({ className }: { className?: string }) => ( @@ -181,10 +181,7 @@ function GetStartedButton() { } return ( - + ); } - diff --git a/surfsense_web/components/onboarding-tour.tsx b/surfsense_web/components/onboarding-tour.tsx index 0fc43160a..958bb43b0 100644 --- a/surfsense_web/components/onboarding-tour.tsx +++ b/surfsense_web/components/onboarding-tour.tsx @@ -1,15 +1,15 @@ "use client"; -import { useAtomValue } from "jotai"; import { useQuery } from "@tanstack/react-query"; +import { useAtomValue } from "jotai"; import { usePathname } from "next/navigation"; import { useTheme } from "next-themes"; import { useCallback, useEffect, useRef, useState } from "react"; import { createPortal } from "react-dom"; -import { currentUserAtom } from "@/atoms/user/user-query.atoms"; -import { activeSearchSpaceIdAtom } from "@/atoms/search-spaces/search-space-query.atoms"; -import { documentTypeCountsAtom } from "@/atoms/documents/document-query.atoms"; import { connectorsAtom } from "@/atoms/connectors/connector-query.atoms"; +import { documentTypeCountsAtom } from "@/atoms/documents/document-query.atoms"; +import { activeSearchSpaceIdAtom } from "@/atoms/search-spaces/search-space-query.atoms"; +import { currentUserAtom } from "@/atoms/user/user-query.atoms"; import { fetchThreads } from "@/lib/chat/thread-persistence"; interface TourStep { diff --git a/surfsense_web/mdx-components.tsx b/surfsense_web/mdx-components.tsx index f6d86e543..9dedbd20f 100644 --- a/surfsense_web/mdx-components.tsx +++ b/surfsense_web/mdx-components.tsx @@ -1,5 +1,6 @@ import defaultMdxComponents from "fumadocs-ui/mdx"; import type { MDXComponents } from "mdx/types"; +import Image, { type ImageProps } from "next/image"; import { Accordion, AccordionContent, @@ -7,16 +8,15 @@ import { AccordionTrigger, } from "@/components/ui/accordion"; import { cn } from "@/lib/utils"; -import Image, { type ImageProps } from "next/image"; export function getMDXComponents(components?: MDXComponents): MDXComponents { return { ...defaultMdxComponents, img: ({ className, alt, ...props }: React.ComponentProps<"img">) => ( {alt ), Video: ({ className, ...props }: React.ComponentProps<"video">) => ( From c9a9d29c54aaddd3adaea41093fb7c32ce583abe Mon Sep 17 00:00:00 2001 From: "DESKTOP-RTLN3BA\\$punk" Date: Wed, 7 Jan 2026 19:43:31 -0800 Subject: [PATCH 72/75] docs: update README --- README.md | 31 +++++-------------- README.zh-CN.md | 31 +++++-------------- surfsense_backend/.env.example | 4 +-- .../content/docs/docker-installation.mdx | 24 +++++++------- 4 files changed, 29 insertions(+), 61 deletions(-) diff --git a/README.md b/README.md index acd900588..03a7c88c1 100644 --- a/README.md +++ b/README.md @@ -174,44 +174,29 @@ docker run -d -p 3000:3000 -p 8000:8000 ` ghcr.io/modsetter/surfsense:latest ``` -**With Custom Configuration (e.g., OpenAI Embeddings):** +**With Custom Configuration:** + +You can pass any environment variable using `-e` flags: ```bash docker run -d -p 3000:3000 -p 8000:8000 \ -v surfsense-data:/data \ -e EMBEDDING_MODEL=openai://text-embedding-ada-002 \ -e OPENAI_API_KEY=your_openai_api_key \ - --name surfsense \ - --restart unless-stopped \ - ghcr.io/modsetter/surfsense:latest -``` - -**With OAuth-based Connectors (Google Calendar, Gmail, Drive, Airtable):** - -To use OAuth-based connectors, you need to configure the respective client credentials: - -```bash -docker run -d -p 3000:3000 -p 8000:8000 \ - -v surfsense-data:/data \ - # Google Connectors (Calendar, Gmail, Drive) + -e AUTH_TYPE=GOOGLE \ -e GOOGLE_OAUTH_CLIENT_ID=your_google_client_id \ -e GOOGLE_OAUTH_CLIENT_SECRET=your_google_client_secret \ - -e GOOGLE_CALENDAR_REDIRECT_URI=http://localhost:8000/api/v1/auth/google/calendar/connector/callback \ - -e GOOGLE_GMAIL_REDIRECT_URI=http://localhost:8000/api/v1/auth/google/gmail/connector/callback \ - -e GOOGLE_DRIVE_REDIRECT_URI=http://localhost:8000/api/v1/auth/google/drive/connector/callback \ - # Airtable Connector - -e AIRTABLE_CLIENT_ID=your_airtable_client_id \ - -e AIRTABLE_CLIENT_SECRET=your_airtable_client_secret \ - -e AIRTABLE_REDIRECT_URI=http://localhost:8000/api/v1/auth/airtable/connector/callback \ + -e ETL_SERVICE=LLAMACLOUD \ + -e LLAMA_CLOUD_API_KEY=your_llama_cloud_key \ --name surfsense \ --restart unless-stopped \ ghcr.io/modsetter/surfsense:latest ``` > [!NOTE] -> - For Google connectors, create OAuth 2.0 credentials in the [Google Cloud Console](https://console.cloud.google.com/apis/credentials) +> - For Google OAuth, create credentials in the [Google Cloud Console](https://console.cloud.google.com/apis/credentials) > - For Airtable connector, create an OAuth integration in the [Airtable Developer Hub](https://airtable.com/create/oauth) -> - If deploying behind a reverse proxy with HTTPS, add `-e BACKEND_URL=https://api.yourdomain.com` and update the redirect URIs accordingly +> - If deploying behind a reverse proxy with HTTPS, add `-e BACKEND_URL=https://api.yourdomain.com` After starting, access SurfSense at: - **Frontend**: [http://localhost:3000](http://localhost:3000) diff --git a/README.zh-CN.md b/README.zh-CN.md index 4e4b0174b..fcccddcaa 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -181,44 +181,29 @@ docker run -d -p 3000:3000 -p 8000:8000 ` ghcr.io/modsetter/surfsense:latest ``` -**使用自定义配置(例如 OpenAI 嵌入):** +**使用自定义配置:** + +您可以使用 `-e` 标志传递任何环境变量: ```bash docker run -d -p 3000:3000 -p 8000:8000 \ -v surfsense-data:/data \ -e EMBEDDING_MODEL=openai://text-embedding-ada-002 \ -e OPENAI_API_KEY=your_openai_api_key \ - --name surfsense \ - --restart unless-stopped \ - ghcr.io/modsetter/surfsense:latest -``` - -**使用 OAuth 连接器(Google 日历、Gmail、云端硬盘、Airtable):** - -要使用基于 OAuth 的连接器,您需要配置相应的客户端凭据: - -```bash -docker run -d -p 3000:3000 -p 8000:8000 \ - -v surfsense-data:/data \ - # Google 连接器(日历、Gmail、云端硬盘) + -e AUTH_TYPE=GOOGLE \ -e GOOGLE_OAUTH_CLIENT_ID=your_google_client_id \ -e GOOGLE_OAUTH_CLIENT_SECRET=your_google_client_secret \ - -e GOOGLE_CALENDAR_REDIRECT_URI=http://localhost:8000/api/v1/auth/google/calendar/connector/callback \ - -e GOOGLE_GMAIL_REDIRECT_URI=http://localhost:8000/api/v1/auth/google/gmail/connector/callback \ - -e GOOGLE_DRIVE_REDIRECT_URI=http://localhost:8000/api/v1/auth/google/drive/connector/callback \ - # Airtable 连接器 - -e AIRTABLE_CLIENT_ID=your_airtable_client_id \ - -e AIRTABLE_CLIENT_SECRET=your_airtable_client_secret \ - -e AIRTABLE_REDIRECT_URI=http://localhost:8000/api/v1/auth/airtable/connector/callback \ + -e ETL_SERVICE=LLAMACLOUD \ + -e LLAMA_CLOUD_API_KEY=your_llama_cloud_key \ --name surfsense \ --restart unless-stopped \ ghcr.io/modsetter/surfsense:latest ``` > [!NOTE] -> - 对于 Google 连接器,请在 [Google Cloud Console](https://console.cloud.google.com/apis/credentials) 中创建 OAuth 2.0 凭据 +> - 对于 Google OAuth,请在 [Google Cloud Console](https://console.cloud.google.com/apis/credentials) 中创建凭据 > - 对于 Airtable 连接器,请在 [Airtable 开发者中心](https://airtable.com/create/oauth) 中创建 OAuth 集成 -> - 如果部署在带有 HTTPS 的反向代理后面,请添加 `-e BACKEND_URL=https://api.yourdomain.com` 并相应地更新重定向 URI +> - 如果部署在带有 HTTPS 的反向代理后面,请添加 `-e BACKEND_URL=https://api.yourdomain.com` 启动后,访问 SurfSense: - **前端**: [http://localhost:3000](http://localhost:3000) diff --git a/surfsense_backend/.env.example b/surfsense_backend/.env.example index ebbc8dc69..2c2fec48b 100644 --- a/surfsense_backend/.env.example +++ b/surfsense_backend/.env.example @@ -56,8 +56,8 @@ DISCORD_REDIRECT_URI=http://localhost:8000/api/v1/auth/discord/connector/callbac DISCORD_BOT_TOKEN=your_bot_token_from_developer_portal # Atlassian OAuth Configuration -ATLASSIAN_CLIENT_ID=V4Axk5VLcsAKJxffMjRGSHtlh17uVswl -ATLASSIAN_CLIENT_SECRET=ATOAmjcoJ_wpyr98F5nF9BVZFDtXpLHs53YnK8TVQhjJh2LuRPYrnDirBwW5lV5cWRbK9B430F02 +ATLASSIAN_CLIENT_ID=your_atlassian_client_id_here +ATLASSIAN_CLIENT_SECRET=your_atlassian_client_secret_here JIRA_REDIRECT_URI=http://localhost:8000/api/v1/auth/jira/connector/callback CONFLUENCE_REDIRECT_URI=http://localhost:8000/api/v1/auth/confluence/connector/callback diff --git a/surfsense_web/content/docs/docker-installation.mdx b/surfsense_web/content/docs/docker-installation.mdx index d61aa3bc8..6501c7783 100644 --- a/surfsense_web/content/docs/docker-installation.mdx +++ b/surfsense_web/content/docs/docker-installation.mdx @@ -47,31 +47,29 @@ docker run -d -p 3000:3000 -p 8000:8000 ` ### With Custom Configuration -**Using OpenAI Embeddings:** +You can pass any [environment variable](/docs/manual-installation#backend-environment-variables) using `-e` flags: ```bash docker run -d -p 3000:3000 -p 8000:8000 \ -v surfsense-data:/data \ -e EMBEDDING_MODEL=openai://text-embedding-ada-002 \ -e OPENAI_API_KEY=your_openai_api_key \ - --name surfsense \ - --restart unless-stopped \ - ghcr.io/modsetter/surfsense:latest -``` - -**With Google OAuth:** - -```bash -docker run -d -p 3000:3000 -p 8000:8000 \ - -v surfsense-data:/data \ -e AUTH_TYPE=GOOGLE \ - -e GOOGLE_OAUTH_CLIENT_ID=your_client_id \ - -e GOOGLE_OAUTH_CLIENT_SECRET=your_client_secret \ + -e GOOGLE_OAUTH_CLIENT_ID=your_google_client_id \ + -e GOOGLE_OAUTH_CLIENT_SECRET=your_google_client_secret \ + -e ETL_SERVICE=LLAMACLOUD \ + -e LLAMA_CLOUD_API_KEY=your_llama_cloud_key \ --name surfsense \ --restart unless-stopped \ ghcr.io/modsetter/surfsense:latest ``` + +- For Google OAuth, create credentials in the [Google Cloud Console](https://console.cloud.google.com/apis/credentials) +- For Airtable connector, create an OAuth integration in the [Airtable Developer Hub](https://airtable.com/create/oauth) +- If deploying behind a reverse proxy with HTTPS, add `-e BACKEND_URL=https://api.yourdomain.com` + + ### Quick Start with Docker Compose For easier management with environment files: From 30401f50a5122cabe0e9f58c51d10a13eda5c9db Mon Sep 17 00:00:00 2001 From: "DESKTOP-RTLN3BA\\$punk" Date: Wed, 7 Jan 2026 20:40:54 -0800 Subject: [PATCH 73/75] docs: setup guides for Airtable and ClickUp OAuth integrations --- .../content/docs/connectors/airtable.mdx | 97 +++++++++++++++++- .../content/docs/connectors/clickup.mdx | 53 +++++++++- .../content/docs/connectors/confluence.mdx | 2 +- .../content/docs/connectors/gmail.mdx | 2 +- .../docs/connectors/google-calendar.mdx | 2 +- .../content/docs/connectors/google-drive.mdx | 2 +- .../content/docs/connectors/jira.mdx | 2 +- .../content/docs/connectors/slack.mdx | 2 +- .../airtable/airtable-oauth-integrations.png | Bin 0 -> 53370 bytes .../airtable-register-integration.png | Bin 0 -> 64254 bytes .../connectors/airtable/airtable-scopes.png | Bin 0 -> 117899 bytes .../airtable/airtable-support-info.png | Bin 0 -> 95313 bytes .../clickup/clickup-api-settings.png | Bin 0 -> 84729 bytes .../clickup/clickup-app-credentials.png | Bin 0 -> 101055 bytes 14 files changed, 154 insertions(+), 8 deletions(-) create mode 100644 surfsense_web/public/docs/connectors/airtable/airtable-oauth-integrations.png create mode 100644 surfsense_web/public/docs/connectors/airtable/airtable-register-integration.png create mode 100644 surfsense_web/public/docs/connectors/airtable/airtable-scopes.png create mode 100644 surfsense_web/public/docs/connectors/airtable/airtable-support-info.png create mode 100644 surfsense_web/public/docs/connectors/clickup/clickup-api-settings.png create mode 100644 surfsense_web/public/docs/connectors/clickup/clickup-app-credentials.png diff --git a/surfsense_web/content/docs/connectors/airtable.mdx b/surfsense_web/content/docs/connectors/airtable.mdx index 1fbe427ec..366a6e8e5 100644 --- a/surfsense_web/content/docs/connectors/airtable.mdx +++ b/surfsense_web/content/docs/connectors/airtable.mdx @@ -3,4 +3,99 @@ title: Airtable description: Connect your Airtable bases to SurfSense --- -# Documentation in progress +# Airtable OAuth Integration Setup Guide + +This guide walks you through setting up an Airtable OAuth integration for SurfSense. + +## Step 1: Access Airtable OAuth Integrations + +1. Navigate to [airtable.com/create/oauth](https://airtable.com/create/oauth) +2. In the **Builder Hub**, under **Developers**, click **"OAuth integrations"** +3. Click **"Register an OAuth integration"** + +![Airtable OAuth Integrations Page](/docs/connectors/airtable/airtable-oauth-integrations.png) + +## Step 2: Register an Integration + +Fill in the basic integration details: + +| Field | Value | +|-------|-------| +| **Name** | `SurfSense` | +| **OAuth redirect URL** | `http://localhost:8000/api/v1/auth/airtable/connector/callback` | + +Click **"Register integration"** + +![Register Integration Form](/docs/connectors/airtable/airtable-register-integration.png) + +## Step 3: Configure Scopes + +After registration, configure the required scopes (permissions) for your integration: + +### Record data and comments + +| Scope | Description | +|-------|-------------| +| ✅ `data.recordComments:read` | See comments in records | +| ✅ `data.records:read` | See the data in records | + +### Base schema + +| Scope | Description | +|-------|-------------| +| ✅ `schema.bases:read` | See the structure of a base, like table names or field types | + +### User metadata + +| Scope | Description | +|-------|-------------| +| ✅ `user.email:read` | See the user's email address | + +![Scopes Configuration](/docs/connectors/airtable/airtable-scopes.png) + +## Step 4: Configure Support Information + +Scroll down to configure the support information and authorization preview: + +| Field | Value | +|-------|-------| +| **Support email** | Your support email address | +| **Privacy policy URL** | Your privacy policy URL | +| **Terms of service URL** | Your terms of service URL | + +The preview shows what users will see when authorizing SurfSense: +- The data in your records +- Comments in your records +- The structure of your base, like table names or field types +- Your email address + +Click **"Save changes"** + +![Support Information & Preview](/docs/connectors/airtable/airtable-support-info.png) + +## Step 5: Get OAuth Credentials + +After saving, you'll find your OAuth credentials on the integration page: + +1. Copy your **Client ID** +2. Copy your **Client Secret** + +> ⚠️ Never share your client secret publicly. + +--- + +## Running SurfSense with Airtable Connector + +Add the Airtable environment variables to your Docker run command: + +```bash +docker run -d -p 3000:3000 -p 8000:8000 \ + -v surfsense-data:/data \ + # Airtable Connector + -e AIRTABLE_CLIENT_ID=your_airtable_client_id \ + -e AIRTABLE_CLIENT_SECRET=your_airtable_client_secret \ + -e AIRTABLE_REDIRECT_URI=http://localhost:8000/api/v1/auth/airtable/connector/callback \ + --name surfsense \ + --restart unless-stopped \ + ghcr.io/modsetter/surfsense:latest +``` \ No newline at end of file diff --git a/surfsense_web/content/docs/connectors/clickup.mdx b/surfsense_web/content/docs/connectors/clickup.mdx index f59030788..1b732c968 100644 --- a/surfsense_web/content/docs/connectors/clickup.mdx +++ b/surfsense_web/content/docs/connectors/clickup.mdx @@ -3,4 +3,55 @@ title: ClickUp description: Connect your ClickUp workspace to SurfSense --- -# Documentation in progress \ No newline at end of file +# ClickUp OAuth Integration Setup Guide + +This guide walks you through setting up a ClickUp OAuth integration for SurfSense. + +## Step 1: Access ClickUp API Settings + +1. Open your ClickUp workspace +2. Navigate to **Settings** (gear icon) → **ClickUp API** +3. You'll see the **ClickUp API Settings** page + +![ClickUp API Settings Page](/docs/connectors/clickup/clickup-api-settings.png) + +## Step 2: Create an App + +1. Click **"+ Create an App"** in the top-right corner +2. Fill in the app details: + +| Field | Value | +|-------|-------| +| **App Name** | `SurfSense` | +| **Redirect URL(s)** | `localhost:8000` | + +3. Click **"Save"** to create the app + +![App Created with Credentials](/docs/connectors/clickup/clickup-app-credentials.png) + +## Step 3: Get OAuth Credentials + +After creating the app, you'll see your credentials: + +1. Copy your **Client ID** +2. Copy your **Client Secret** (click "Show" to reveal, or "Regenerate" if needed) + +> ⚠️ Never share your client secret publicly. + +--- + +## Running SurfSense with ClickUp Connector + +Add the ClickUp environment variables to your Docker run command: + +```bash +docker run -d -p 3000:3000 -p 8000:8000 \ + -v surfsense-data:/data \ + # ClickUp Connector + -e CLICKUP_CLIENT_ID=your_clickup_client_id \ + -e CLICKUP_CLIENT_SECRET=your_clickup_client_secret \ + -e CLICKUP_REDIRECT_URI=http://localhost:8000/api/v1/auth/clickup/connector/callback \ + --name surfsense \ + --restart unless-stopped \ + ghcr.io/modsetter/surfsense:latest +``` \ No newline at end of file diff --git a/surfsense_web/content/docs/connectors/confluence.mdx b/surfsense_web/content/docs/connectors/confluence.mdx index aa220fcbe..fad9f3e3d 100644 --- a/surfsense_web/content/docs/connectors/confluence.mdx +++ b/surfsense_web/content/docs/connectors/confluence.mdx @@ -85,7 +85,7 @@ Select the **"Granular scopes"** tab and enable: 1. In the left sidebar, click **"Settings"** 2. Copy your **Client ID** and **Client Secret** -> ⚠️ Never share your client secret publicly or include it in code repositories. +> ⚠️ Never share your client secret publicly. --- diff --git a/surfsense_web/content/docs/connectors/gmail.mdx b/surfsense_web/content/docs/connectors/gmail.mdx index 6c08804fc..434e6ae4d 100644 --- a/surfsense_web/content/docs/connectors/gmail.mdx +++ b/surfsense_web/content/docs/connectors/gmail.mdx @@ -60,7 +60,7 @@ This guide walks you through setting up a Google OAuth 2.0 integration for SurfS 1. After creating the OAuth client, you'll see a dialog with your credentials 2. Copy your **Client ID** and **Client Secret** -> ⚠️ Never share your client secret publicly or include it in code repositories. +> ⚠️ Never share your client secret publicly. ![Google Developer Console Config](/docs/connectors/google/google_oauth_config.png) diff --git a/surfsense_web/content/docs/connectors/google-calendar.mdx b/surfsense_web/content/docs/connectors/google-calendar.mdx index e6ae4d593..cc1eae545 100644 --- a/surfsense_web/content/docs/connectors/google-calendar.mdx +++ b/surfsense_web/content/docs/connectors/google-calendar.mdx @@ -59,7 +59,7 @@ This guide walks you through setting up a Google OAuth 2.0 integration for SurfS 1. After creating the OAuth client, you'll see a dialog with your credentials 2. Copy your **Client ID** and **Client Secret** -> ⚠️ Never share your client secret publicly or include it in code repositories. +> ⚠️ Never share your client secret publicly. ![Google Developer Console Config](/docs/connectors/google/google_oauth_config.png) diff --git a/surfsense_web/content/docs/connectors/google-drive.mdx b/surfsense_web/content/docs/connectors/google-drive.mdx index f2b0105fc..00ea2f610 100644 --- a/surfsense_web/content/docs/connectors/google-drive.mdx +++ b/surfsense_web/content/docs/connectors/google-drive.mdx @@ -60,7 +60,7 @@ This guide walks you through setting up a Google OAuth 2.0 integration for SurfS 1. After creating the OAuth client, you'll see a dialog with your credentials 2. Copy your **Client ID** and **Client Secret** -> ⚠️ Never share your client secret publicly or include it in code repositories. +> ⚠️ Never share your client secret publicly. ![Google Developer Console Config](/docs/connectors/google/google_oauth_config.png) diff --git a/surfsense_web/content/docs/connectors/jira.mdx b/surfsense_web/content/docs/connectors/jira.mdx index 9d00a56af..ebe639d6d 100644 --- a/surfsense_web/content/docs/connectors/jira.mdx +++ b/surfsense_web/content/docs/connectors/jira.mdx @@ -72,7 +72,7 @@ This guide walks you through setting up an Atlassian OAuth 2.0 (3LO) integration 1. In the left sidebar, click **"Settings"** 2. Copy your **Client ID** and **Client Secret** -> ⚠️ Never share your client secret publicly or include it in code repositories. +> ⚠️ Never share your client secret publicly. --- diff --git a/surfsense_web/content/docs/connectors/slack.mdx b/surfsense_web/content/docs/connectors/slack.mdx index 838408cd7..ccabe6f9e 100644 --- a/surfsense_web/content/docs/connectors/slack.mdx +++ b/surfsense_web/content/docs/connectors/slack.mdx @@ -32,7 +32,7 @@ After creating the app, you'll be taken to the **Basic Information** page. Here 1. Copy your **Client ID** 2. Copy your **Client Secret** (click Show to reveal) -> ⚠️ Never share your app credentials publicly or include them in code repositories. +> ⚠️ Never share your app credentials publicly. ![Basic Information - App Credentials](/docs/connectors/slack/slack-app-credentials.png) diff --git a/surfsense_web/public/docs/connectors/airtable/airtable-oauth-integrations.png b/surfsense_web/public/docs/connectors/airtable/airtable-oauth-integrations.png new file mode 100644 index 0000000000000000000000000000000000000000..bfe301d78b778d362c141b5278cb873de74954a3 GIT binary patch literal 53370 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 e)4ybiTae$k39Y8%5RHerZF#P0 literal 0 HcmV?d00001 diff --git a/surfsense_web/public/docs/connectors/airtable/airtable-scopes.png b/surfsense_web/public/docs/connectors/airtable/airtable-scopes.png new file mode 100644 index 0000000000000000000000000000000000000000..f5c41dd245ec84e70aee7998918983b9ad02dd29 GIT binary patch literal 117899 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 XR_7x9j9;CnpQ>_4^LEKit7rck6+B)e literal 0 HcmV?d00001 diff --git a/surfsense_web/public/docs/connectors/airtable/airtable-support-info.png b/surfsense_web/public/docs/connectors/airtable/airtable-support-info.png new file mode 100644 index 0000000000000000000000000000000000000000..d556a6109370bd2401cdad55072f1303685a66a9 GIT binary patch literal 95313 zcmb??cT`i&*De+;*j^P-kfKtg2q;xhiUl!1=n0*u^xi=_7C@;QI-z$GNNAyi5)lCb zLl2>&(h0o<0)%_e*WY))@2}G5_M+p1wC+m+Uc3T zD;?eW#>4YSi(|eS9o<0==*dH!S0)Rip1wNfV+RKmRqvxWV!C{+dc^-RN;(t9sDHe{ z`1q5LhEJa8FJZ3py}8NoEA)rs#WMn9d^fL$T0J=~en#0j$1F$i`0K96=dvv=`~y^5 zzqfK{KWZcIyka95k=#3yrCl$ree;ncnIpSb&Lh&&(vZ+10T@jF2q4zKhjLVS_*r9| zo2CVb;rw~t?L&$G9D!QjIy==H8X81T9^Xd2Yzl4m|64>yr@*gS@=`JJ?VHEkiQ#_# z^?RUJBeyoc@Xn{Li4{33> z&1fxx>WZO%BKHn8Ve;|Hf8<*G#@*Ut2gh94d8BW4_oTqx;u7=;N&d1140q0sP7m=j zuku;D+hNjO1I9L=_TscHF3ZRK9ZZD=>9FtLd3%COZxSn$gz3kixsPF4;%Uk_##aa} zKNL%iGS8QBmv1fHJ1ZT>{*^ZH<`2^f%L{y?@IBi(1BJStvN%UgQ4s0qJbx=bt|YJT z#?jlBzHH>(>E@TwqD7MCdJig9QQ){=NqBr!2JjL`aGuZ-a{KQZu_cU-(|4+DC5nl~t!FmptO z6`{kkneJOy(42ZHs%_lm%dLfO zXWHW>me09aqJ%5T1*AuEzbhcjL_xgKqQ~r&&sWa9R6(hERtOYW@Z+o_R3mM*87|V| zj#)%gmJLw^xhB^a`bCoZb%e_)*Boyh|Jy{ZU@#t~B<}FsPT&%+lB6axq(}<_LFq_H zUI`;j*Jl9d3p-)aIXR}^s7lUiCoE(*TKtF*%lNL~bIP|`|AxRnjwQgfD^m)r?1Wbn zwyN1yHt%}vOWnv);Hi;bZztdKvTIt8V*J~rxxvK)zdFxO2YB|maYsoHrX<4MeOFCu zw(chvBM?^+>vj{(%i&;pt*l@_`w`cf4%V8`WU=iO(H(wM-A~WX+9l7dP+Y`NXrg;< zTE`oOiPCgQC8pwsP0_p%TAf>e)(HUk;+y#;)m^Mg;+o4Qetq{cL7(vWrc1P~dG{!t zcjM23i1`owKs}r->Q`$VkS29Ynopj*V^tamxMx37JVrZ+7%$Mp`1GMX(=r3zg5j9> zD3V^`RvRpro;T3h(+q?fbP-Fv^$R}(BZ7He=f$

    REkQGT6;+FFOLvDsM3{UHF#CDIlJM^I#yYOBUa}pz&dNZ zsp<(CC3W+BH()c}Dwk)%yT*MybkkBti85VIu#Iaduc#4}oU9?=j}T*hiYL$#<(WFM zbB_8%E`RXA2Tzrb9L}?BNpA6;7&tiZI3t!_{ZqobK3Gmx76U#wEhg^q9182V2ms&U zO6#6J@=AOB)}fm| z8AV_ytnx$)E57af$3keUO|+2M)B3#H;{DxEWXVzmpQz8dVdDa4=H=E#?fJLwjkEGSy@ z)-{_VB`(`}osp-eN}s~mcEYE@v*vak(KJtqShWSs!ji13Pa+rchA&lG_w*$uZLP{x zMxG>=#O;2owJK7|^Mn%L!mJKUHU?in7OCMJ5pG+Qp0DT^GTVcy;3V9$?xkJD;JH4Q z0!>6e8k+a*Q>WF~vCWMZOR>e46dCPkxiH=Q_!8;409|c*)@yYid^O5us_KFZ#3Zce zuTZmS` zYY~!{L-v(^5!0bnWan|TCm%OoSC&wz;<2Wbdo$j?Ws{o*Q>2(y^*I1@qLNn(J_X@< zB(~jgkRwhQ6H3*a@hs=m*shMbl88mc)?rINJCkX>0ng90f)6(+7c_vGBI3+lb%)o> zrY(lIIM+-rE-Vi4$$YY(2c4{(zT_Qy{gXZMS;*!xf<P(-509V%aQvZN{BK`&hk zlYCbabzls3UsaWB*X9~VYQ7(?Zt+K^bso0}O8N8?i4>HvRK)@XpMGsFr~S$q#=Q!r z?l;e845O`j`vT^VQTAWT6ZdLOvc8=p-rXov<6*fUZb0t#2j{$ny*lx8DDQ%WU&dP- zB!0uywj*l9gzL}fYIHmicSwKVe6Fv?QcCu&MNH~dG3E8??;p8~4t|b@-uG68i#v9l zJuLC2EaZ;K*6?RKA8Y)y)@k_k^@wyCmt%5!D07giYpU|Am`h7M6z6w%*=tX%rZW7J zN^cmrR?-YRX>*<4+gp5tTG8fPmmYe{#ywuhm_V|mLOv{<^&TB&?PDOVik(KcNU4}$ zARtX{Tbc!U(>Gi1(tKtgU_cHPZva~qp5K;Ep0*t=E!@9g@9*t6!O%!*iCGM>eR_VX zx?|IS>eSn`Gzro@ZuLFi@Xpz$g!qlYv@&J(=nI8C`O??8H~ZJaPN3SraV}?drD>!g}k4) zDQ#icJAbk{Vs3ZHK>Mq`Uc6$@^LK*^UF%S?WEA^FUnf{y*DTbN#%|hS*L_U8V|GXJ zEs<9b4##>99?Y9Zb!wLPXZ7xaY)9Wze>*9$`?^q|y$@JnLoGd7y$88nmqQ?%6}b$m zGKK0Een`E|p0Cm=#xrJ|Mqu)%6x-9PD7FR#BGHX-*RrC=vAu>Rm3%lxaRtIasE z`Vdhgu3hrki2X+&KBczAJs7VX_}%|d>fm*k5FFNH>~z0t=tK zNQs{ua=Z|7MvlE4tYvXN@rl29q*Z1MXF!i_MoP`do8GD{C6`etOsFdXW}1<+n}qMM z)zDE)xbkNBn}thS`(RCPO3|2L0MEYMH2=B1HEs5Lz6#3V?q5p0-&*YY^J5Qgjd$HK z9~$XD9XE-v(OR@0eEcF0!>rm?hqEE1&5id=5rHjZA}I?%G#p zdvO*KaY6^g1pKB3<-8CNi0RESxAAvgRKQ})OIuV-CLEPn`YIjaw{whCFZHt29BHs- z0vHfL`D<}gvNbz&??sJV;f~{4_--%gusBrF;f^xm*MU12&^tLejsWV&d^psAJ&0f}QOt`e ztZQ$cJ!1L%KJO!Hw{z`9LpOn{An$PJ`%jC|R7AfFRoB`v2W=pVtsJJiE~+lI z0kgYNpT>4p+q0#wzHO1s;ktK&s-MdpB8QWj`-PpO?uOYDtg66$(Kg7MmcFiL>jL&8 zMWzN8yIw}!_7|$aUZa?j7KsN;FZ0#TStCPkM&h={V+M3CfM=)jEm8`=mJ1nM+`8sP zrZ&@9hu0YGL)VJ9nB?mLj&Nw7$P25jcZByDERye!$NLSr--Ni|OeSSxZQdzg5t(M( zQ|@_6@J#L0nIY?w8Cp_$4QBYQii|a1gGg>Y155w-tcEhOF5%?Ygeys>{%GJB_2}FV zH^2R@T@`Wbc~#vN@s463iRJ59m3W-GO;Lj+@6#jQbyB* zW!do94>>DPzzH)p(OgE6JDv5u^!TWUt~#VJ6y$A1&a{%GCqYQVd>eVCs5{;Fgs>Ct_AZ!g;~K~z> z?0AXKgsUvxX!9CsDZ^pedh#uG*IxGlX1quv26OQbYWN-lTc3ietQKSK``0#?1_{{n z#Xfh84DL0GTQd=G(+LgO9 zo~y{_y`m8AIiQO7LD*GYJhzaKdo8VO=Do?z55cG{L!M0Me#Wtm3qp*sk3#TEQkmR! zyGw@E4o&uyRKIG6cnq&{4;ZnfAMDtbyfHu8G7-(Em~(yHXzX6zJK9?Bg^j5^&V2Xw z>E;_duASUM`qX+kL*QIb;HXabChsVAOw?gG9D3<|m;xZJ&61#f6a@ap(CQs|WQ_Da zu7Lq<_u#n@sNI}uI84?W~sC`AhkNPx~KZ4%y?iO^~`q6yF=w6#to(> zNb#~K&~s(FIUR`^UXhC#!$+B}PQNn#bit+q)p@T7^KdbgK?m&adB49FEg}1cNV~Yi zgpHh0SG}WCnX36xEB%q}{gYD@Y@rHf2=AT`vidWV*h}td9Xz;`wTBz7Leiq#Xt{35 zX`Gdv>wCZnw|q5!BVZX5Vn>oz0dSg1X*dRE%1@fVSBi}%C4~W&u?AJ%Dxv5u0`^TW z5O!FV3o1J~vy)Pv+xIaPX}jRL;@>%|EZ5PLe3h*;iUP67DMWqqdOZvzF}V z4|Nncelj0Gim7Yrp)1{T2)?$-JcoO+UJobzW7ofVaWN|mdTHVY$@^^=gUKhtobK;R z+I^=HjI1KPNTXgm{?TklFqL4q0k-(yU`O8-yZqUa)#ZS$am+)SeYjCJy4>s)&;CNP zu-8}S6w%1m_(JzLDZRH_CgZY{wd$1h$tP+))H@ODX=w5Y8^YD^d3bPn7pnhkV-lN79y{PqB`2Wnw`inkU8O#q`PgJNWPDP;GVviv{AZ+nyFPutBkCEb+gTqrQ;6kQ zv^?d#D*_0R>5{hua~#Rh-B5qX7=Qd0gAf0TfN z7YW!9$*hI=U2Fhcc*gDlIf8>)hEv9?R=&_MR~9u&U>_4*eC9mN>vUk4%yuj{ry;JS zRq(*JnYm@xri{OfqzifbW=cCF^SeJy#%mFYJboM`**a<7m@)&NsPP%VhRYDG4^Dbh@%zq1G zf^4I-{-GNW{h{mNI5ZI@KerhYw(iXyA(#}@98tTkCo%Q0tHS05W=X=q<#^-n`Mumj zFQ<(No6*}r^p?o>p|XEWqV#``CA@zG^0nQn)21s+Gcv6Gj>inrS7seW;fcyelYNM-S zMZv9Vb=})7>mjd}E=!lf`ZsE;Q{UD-8jx1R`lXB{p~sHiRZ}S{ywhW9{KMarILc&3 z(z1HbRT#(i(6nEpwSL;MpY`+BNAY3oMUM*XPI7lB>7OrsObqNpx85Bqj>YU=OCdMZI|*1jkI6R_iJ z!x9i;jam5oSM7YE(Ix+M?o{RH5rvd71i#0?**LJ!HtNFQ?>Z!c@h$Q)w7A39pt59o z7!~~`4C}#ei0j))>Juy$LHAJC4%)M)e8_o*07<>lt7oaP2<+hda zobssU@wD8-^9IGOt)dXboMvkx_R@Mn8>oEmu@eOgoPC~=#P6T1U#x5_KE&X8PrIg2SElhlb+kb!7z zkfFI#=@u@<@M}q-pYhDkyJ}m3gMKo$EnY*gw?8FhBG;vgHvQEAo8loq=SpI%aiB(E zb;D(nheWm4NfwZN?{}6!;np#5EaZL?@@?9_Hs@EFFBShNttXbrKm=7kqzyiP2AY>| zxZAXvXdih86VJ<JK%~5D7e?@O$a!2xt^2B@I`B7995aQ-v z@kxljc_Cm5D!05CeX8kUg>2q=R>ZRv#_c$`FF5*sOWCdrrKmSV;d<^JVAsK$&1^OIn zHS*aO_R!qGUQu%0RSkk(dhMiU>VixadS7~pWGPWr@Ad(5oqcr)=a>KJohhH(X+4K z11Y5*&N78OC43f<{r%?m8?{13{4FsZOT^Dh6Tc!koFNCykk3$~pNL;lOsP>LWZ{=!x}GE!wXf&q%@4iFg^A>yG3 zFtZ3}AKxj?vbwb^e3hpH{^bHbH@(ER8a%p`pLuo=2`vx+;a<&UmY@_y@(u`5KybGF z#RQctuc0H%EiDwsnn0P^JQcW}FMbja*WurMzb8ZrC-AWTbd>(2y@_e!)jzY-OR|&X z$0*{3gZA7m{pmR!-IOvvwZ>ahcRzD`GLNbo$)Apqtnfk~SqO4Of?^|+loxe61*VbX zzn*LxswJ=;DV7QHWKEpZ)cwrZVz!@{lW_e31r3Lgk6rs@0} z9Hsk_D`*~|)T!Pd2*2?g?R{dB@wQC8znBud8U~C67!`d(w7%GsP02KsMd`1t-)*&% ztnQ!muoNB4d$qOBp9z@egXdGe27mh+Bj15X`0V)jfy}W(r~FkiK7T*M&~eY!PPp=o zSV!E@58Ze8DRq_I_xbN# z*B$NPx1pim+XOgmk&m>akXrMfMA?s?)I$T`E(Yat#2yZ{u}$KT=VB*?hZ`_zW^mwx zP#+W6sxuV#A4ZK%@f$Bl(&w9PS)y4qn2{PM0xiS$5Hi|UPHW~$YcNOS?bd{3$y&qz z7jpR>{;x=MbQ&k_EQ3{(jEvdnT=QDaT-HnXXA>i;K6 zj^4d{*Py~K@#V{x;l`K#n~=V{TZ_FIJPo-%(@7@50*Li~yWx-jvxos%(z#XC)cSM5 zhzCQG_Cwi|lV(wpz3eGVSg&$Ag^M3XneFY4YAEEXr^Rs-(Z<6A^o(Z^gxvCS@tnLo zK`t(BC<+2a5$_f1h$0BU-`e_mEFOG?lIa&1T~qMN6+W3gO=c;Tr%!XWwBWIDI5IGhZfAG5y|YtS zPEJk~(cYYW7if?-Lf5{@jasHg~1-92Qt^T;F&86XQq5q^vuCg2w@Vj0=kxaH;XqL+(X zHDAol%;f#}aXlyqqV?*PL?S^B=etvihT$F9A`QcL;k&<8RN&guOj>}Xq?gmO$o&8z zpiza_W@cvdznWA^4T^y545Wb0DSqvYp@;+m;Ai}kX+uP|*#$_G|S! zeTWIVg(PunfL8f2)>+<37kaK&?wDf8+*M5@QBhI1sWwQ^yLWu|?tL3D7UsE?$H&hP z0tB9%nXz1G27mTTWT)_iUdCGe*% z7QAd?z`0Bb#Ku+9QF1&-;ty}4yDst@6m11xzdku^>du|60mu^li4y|c+)qAy_|PoO zD7Ms|49w{;a^zw_Jm+9XpOZ{G9Q>wtM z!lSiy0Orv2%2@%O{Pf95#@xZW(^Vqtk$;AhZt~rs(MwMTfk03vr{eDx{!FzO%LWU7 z{CJ*|&3&y#sGg7M-~`(02rv%jW@fAi%_mhNXM+Fj(bbEG z3MGaMKrMjaW{Z))sHmtyD5``-@!f9e^D2?wKn?sf7pX=W0+fjcxx*G9=nP^ zL{%yZdh8rGRVs+X+a5mU&yim2V6c6Sh>jS8ZHJB-?-h6~joB^;n>@B8x7c@wO4uFj zY}wa|R+Tur$@OU{2ACRxq@IOIJ`wb2w2QdoQ5{22Uf-|Z7Ln1o^x@;>708F>CR^e1WygXv-3 zNUgOQ7zzRQ()vW(<8wLH#%>427R|y}5Fq7(S&^`1@QwuYAKCdJq5BvM9-i&k?D8<5 zjS&?Lxq7Zw+-~66=)n?)vG)6=h28hgd_ZM}2D{REb;2+4^5OHN(9Zg}Z0s|Yt+ z6(e^UyT@Mqku^D~>bFPpZ(6!10%E|LTZK=(qnXv+jGm~1az>kpwoXsjee(6Yjmzpj zEjwb0qUFs%-EEwoy!Iay4Eqfzy1-KOz!#Y1iXcey(-vkOr--#RePFaYtE-;@7j8b7 z@3(?2Z?y!y9T{TFtt%R^7krTWf#W}rn5ac&)L*u++N^|`6-n1xpi>uPdovp(#3zFS z=628N+cF+hO~bsWXsjGrMxEoE7lo)_oELuSp-3eTYxCC4k=Kmh@_~~n75*cWuh`{_ zx+t7-MK=b+0Ea?>f6}tdCSfW@U`W*1o_DYhN#oy15^4*FVLl2-5UV_(5{^l&x5KzP zBrJz?)+z04t;Ncb^Tsm=HF_d~JxTerI9K!p>7I)|JCt2#U1$}~EzcRoF<)stnCMF z9Q!My7DZvw*IOUS)xTNt#?>5^66vZ-*%fpdI64t%_!Wk|B3E?LKKrfV z0%j*(OpC}oD(<{=++E`QNjkb5wcOUi2)UoiqB~VXcB1@CcJBIxysdEsw)mp%Puh@0 zKi)atCkb$Ylge;aNyL{9o>sffLJrBqY{E2h$O2tBC5Mf*T5}JNe{-qV=!Ns{dsC__ znQQIz>9|6udFp+xab<{`PuLl%%JAD@P}b`QRkivu)Y9^9*wCP$?eMJQcx1iZ$e$Fp z^sjssE1uoIro25@^FnP%Pw$&jY`$;Ltkp8(SL+wq~c$tf(K~g%KB;*XG^3C z{1U594=pvKoRZqCd~P+GV4MlG7YlK7r9)-NUI!xF+=AVVpKaBbjl76cmMhmvO<|drB1* zRdLX-dKXXWs-JK-cl}y?L$`;~O$bbz!3ZuIz(spI$mpb2Tv?lY4sV+7(;oeO{*6!? zV=A{Tm7XUzN1*qmGdn5S8ZrS><-lZQRhDMQ{J13W3Jy10x21Ns1vgd63+vhdr!f*)BKsi7YsyH$k_+u6P! z%iZdtkFVXpO?Y3ee8t1sa^;(Oih-qF^v0+;k1|pZn_2=OB<83jNOpMu^K&$yzu4iR zJ3aCMqL+Xd2%_gJEZt@1gTXq7hJUp4+u&S6o_<||G=A6imieCh$r3YPA_(&SmYl6w zVe7r%o7tRPgZvO%h@A3)g5LX+@|Tl<{IAOq7qMbJ6+d!+?l${J{d#TL^I^s32HY-_XIY(~ZgbT@j` zuNk_0@>j!~p}x603~!=lp64)8`iS%{Z`YOa4t9Wg&v^|-vcVU;^>Vaw1y_oUcr+l$ z6G-J)1qk}mb$NUtHjsy(e={jHKQ6=BTGy>}Qw4~FRK+dH7nC2HVRYGkBS3RjY;8eX z?PT=IxXo6b;5F&*rCHBIgdWeaq%t-0f)E$Ray|VQvd--GnAauUu!Vy`A!u8or#+TB zv-JeOGHEh}81Cz!pW--rg;(RZ+d;>dj<`faJ~{b$405*JBvvL9$^SgYvnf@_)1nPp3>f{bR!HKT*Ex z0v#7cwa@)YlXWOXFw@#D(kXc?VHCsSuhlq2;6^vRYrNsYLzGfMq!OZh4X?yQ>`LJ}X-=*&k2I-NCmLaj zW^8Av?`6cD9G$LY-uITzWahBf^xjWpUL%>akN94utpGC+s`!PdQHR}fsM`Unr_>p> z5*@@q@$)({#1?K8krxO-)cafL+e3ql_8vF-p#`a`nR56TB$*inFO=LbdYb;GaRDD^ zzzsPiW~+f!Fm5ZKduEec4o!fw`%dD<$YJn$ z=7G8V^ck`82j1x5<0-w0jVs!Yw~0v;bI5_Ei$xw00S1`w2o%w_zH8n(nB5^5)^F3& z?N$J_B<#LKn*;zNh%UpZ{?HgV-qw3VH$c`BC+E0kwyyxbPB6=%NcNO?Z}%eS2LSHC zs=0;XM2oV6@n7Vu=%U70j!8iTI8@#r3`T_T^Yeh-?!o&&_46#O>AT3|0+Q0&cy9CA z(574&CcmkB?`%S-^O;RZS^h+sG;88$?9o2FOJLG*Dl!j-6cu7wqfj*4`yuYLpB42* z?L)&T-3LXddX!)Z*n@n`cS*z&Hd&Y65IS|Y?V{z1A^Z!}QsT~UMXGRDK=&sm;4R-~ zT^@ou)~oMXk)IdZx;j+)%UyQwpuUg4(plwToNR#GO-9obwMjm>0>{czIgVbsrk+>a zB8Fs>cDmSvMmaF$p%IHM3+{+7mn&>(laH0f&y&;$$+e$d_hd)=D*a@GOoj>^9b3nX zx2&eiIBiC|xeL6{jQXidZPsx;aYjvBO|eYq<*86#{Lq21%8SSU+O9SAze2C(TyU zVz0umotC$9t{PjN004DFYxcP64P~)d6wLFdPBo(BPk1cd=+0>OoQdTpdOF5}|1&(J zP@jC?(?r5XM@n+0^`j47RI;_DC40t%7HxnnKuV->c85m!M{^tF{2{yQ&HTM^{<79W z)+3Td$l1L450dN!y!1Os<@cqv_XSVRYAEM!rGZIUM${u!-kqQXVks^y=>%1P>XJlb z{FNsbFY4~|5hEO&Aihxwo{7#@4O8^5_s11^a0LbFblDf7=DTP>1i|%S(_>1C!h%@K zzC$`+Em&devoU3Of2vG-%0VkuUL_(;YKzgJ#Q1)U;C~_>O)u|CPMyyA2Y?FSov^0d z13Ot%Bi@)eHgE>L6&>F})++iZXTuhg?K8}uqj`?rq{2qmAh%vN1-WuI`Y3^y{aLF{ znF4GReT7>)Utf-ifUEIHPddJB6X25j#a9)X#JxYGjC*-mEY_lAPNx@#>svl=TvM6S z0a+&vemyQ>Z=yfb$=hy&<>$T*4zPCB@Wk#JzxLm1dmf`5krwZrqjIrxJjv$A`)YU>$0%m zk`oJL5_xxwiswamVaCc8^S|6tkI(};yEyPBTovg=%te8q#wsO9S6r0c6(NQTtD4Bx zT-}ar<&1|6rGt|`N?bc*7c-P!_{#(c>tDH~WhObe(FLopH5(t__y(g`eNHJ6t%hEH zsvlP>4Mq6fpVc0hq^grb+^5=N3p<)4|LJErekHGMbfL3baFkZ-kg>jG+q3;xYli*b zonU(Y?|pxImrIh3wCnO+Sz>V}vz&i?$M~?8J31}?Hp@@~oHxxCDpAjEEE}Eiy`33I zleW`p<_Qx{?a=tj(>hZMwiWG}GKCxDBHT#_Zp4Z?SqZ$hkbYzufWl|bh_UU~aBdhZ zY(4}QqrMp_s%wwJ5rtdL*8Cm#eCCsm>L|Sq-Sw)hFY<_&+qBPmEJ*S`FA!<--0}xG zr_GZhVZCbYGjN~wasTcgt>XMUq@(aUXh^iL!y97ZNW@Aa?4T%QkgiSyYsS1(X0%7p ziFubb9-^9h&k+@zKS-TujshQ)n6=rIwFw|9^hMo>HrlENrICiBeLP|iU39j!YYyGM z`R7F&J$$K6e?DKUneWpa%WWG{i6oAhQio<6&ETX+#S@z`wNoHt?LY^ypel}K4w~I6 zHULjLBRRpUJZYQPSc%;3A0IvNx$0e?w95dsmw9b%!&zPs7XyX)m5Lj zYU9!8oXbZGa%lE4OO&3s>foRSL)SciAEr~U+q9pwH%_&r!FxAk@zCQ9_^Uc~5Nf@)7n3M)CtQ)zy!oy*N% zL5ZE!t5VL<*znqJrEYIP1SB?q7FEBi%~fQtqlh0LSIom1elItc4Mv=o;`%My;c+GEA6>d zYtxtS0Hmqkp!l2HY0(?zvQjO-VZeU-)bTT=2KJ?c*|IGGy#+oF(MNqHmLunFhI7$F zl4rFw0;9~*E*b!@YVk(Lq&j@UVDt~<5``19E!i`pgUBm*%?3l!D=yW zYraiwHA>1JunjunQ|W%EViKllbU*Na3r;uZKFC-*B#-DlMki5zEDvRz ze4sn}%l5@WT%xYFRcX*aQ&h?0tn#f)!=NzhrC4KqncMY zAaSZFn?43fk7_gC1&yJl_?UXf*FBVa^oFb}J7V8z#!DjS5u|T#=VuRJTiF#|l%OS!n*8YH6B(jgfJ3RS*8= zt!q7P=};nGkt4+;{U_uk?K6~imu&T4i`eb}Aud21c<67&Qt_A72!x1a&;0lgzVK<~ z8^dVF^vQ`^iM~A1>j(OVc^~Eb?oK8cBGP82Cq$wH^!ksCCU8rX8SByy<~5dphF+pG z1E^1DudJUFfUBIQ6aX(JzR|(jK%)F_QLannX-HvO1Z)BQ=3;bmfUnrqFsg-9>)#cq ze58MmUc*jr9Uu}4n?f?R5;h->lJ+FpbCdeTQ|_MpHVuVbW1K`mD|Lx^F#Fz54q%=D z7$K}2vooCOkT2TP(H7@5Vf~@*F5;L@fr~8gHX&1)gxXu0pH{!CZr_hQQyXK<2j^Fj z%H$-x+7%I#@7j8Zis4Au=EWw@tbEKKp*x13jgDM@1Bt^ot3H||FR-6I( zvx5Hm>sNT_|Ay%slMZRADt^_{0@PGyYQ@(%^?pH)%K~}XX7iuN!Who3x!YpHq!V+6 zVTnX{`)3Umj=tE5T0euCwcD5%ixF>NQxkgT-G}-p_w{+laRb?UZfO_<1`cSUu-3XQDa%_JL+qLc9Lk~GV~OXnjvd}|H!77Ot&xrq z-qblfMx3rEAGIDAvu6p)$VJMO}=$=Nt!6^#dG!3Jb zPXWG?7fc0+F9Sz<`5)_5&y(cKWRFI3N7r4de5ny?(0@PYW1#qiMk3o!r@VQ81F3vm z|2ZVu@g$Uld%bmlsS`0m?LxE(*UNf(ioAwIMGN5)s6DC1PNkN4NbDQ%TG2cgNcqBQ z?_3yeYCn&wEgnftHsgF7(X9%8{lix-b_`$qkP=@mTP~-ZkJbUGQ`j?aHZ)e|7l!9~~ZQ{r{-L;oS7p9CFT2TSk+)q{!p~bb$Y= z?qcZuOl`^t@3`bS^D%!7IYqY5LuOhQEyFP->z)$Zs~51y@h1m$S@j)#`P>IK7KvYc z%}a;e48J=QW!$<=$79DwW>)WE7+e=W)?`*y@FPs!aWnGa1mylcf?=N7+ODH=H6(kK z6kZfmR`j-n=OFag(2@V)q{bfNpD(8GY{33F#Te`FDJV2NqREmd2XEzW$0)^CFDLXo zJ{oh$sNB%TDNvNcu05S`8O|LIw(Et@@ZNT<035TSEimUE|2Rx zX^y*j+W(KUpnOIEpkuEI1ia3Ctav_D`33U0D{{}hbZ|lY;jmp#>Ses(l2NVmvy9{0 z!;{N`#_Ey?GsvUD^Wb8_g*DD(D z45Ks4i-Sre4no(4GWLE^en$lOyz&qEvCrVxi?gV9tl+mD(4!gF&%Du-Go~1tP+O5J zf=q2WHlx#UTNdFAz7e@aCofNuPPBu=aEfv)^t)46b70QL@rp&pigTZ2nxVfz9{7dp z@Tm%~vvoE|bIcH)GTF_|)w`{vk}AiyD?`Z3%;>fTCMIT`hFxZ(hxMh;5?f8A%6K5L zZw2#RoD?DF(fRvIHsq{X-nBCf#5ct^1`|Br*VLwrWXop{uoYGb#3a#PJ=&!3UiCB|nz@uO4)9!RyAN=YjQSQOGaKvL`v1b0>o}bYoZ!`!<;$RZk7OeFyZh_wYuAcasnBB#z zrRSRX#UYx%Fi|z*q9`O^WG95b5Q(=To!1=2G3j6&-p>=U0mdkg_cp}y{f(Xng{%Ai zUA@Qf+7ZPlhWe!3;F1}g#Q`QFbsyd$eN-yUCw6&usOieyZ&OJ3hxJSE($W@_piHHB zn>UN#;u3^GYbAqbaPa0es7}^<4f<^a(+|fuJYJvXZ(*+28*5F7QMG8D^QV6nRnLa~^3B z@@Bv{k)r2mk;fUnuxtz4y%>M0t?-9ob~y6KLH!VRnqT=|JOJ`}tLj3_+s@RGaTZAN zZ>uBYCjiP*=b8Inz?~wUl5M(r#k@q12SFfPlo+^E7a+!}&mdoR7^4Vdak;UG;W?80 z{2YS4k~~}O1cJBn?wA=wnm6onyT9AKysvw{LWFN)X{|9!)TdZZ*?>FQo1A4$uEu zj*7J?J|9d!`{=E~+6OxX*(2BAo&D^YE{w$6*h{lq(RMhJi&|aLl@0m0S3v`}OIx#5 z+yw**T+&x{0&kWTD{g)M+bxZrgr!+G%!Il#okscpplr2Gg1`JvM|!PINpdva3z}l5 z8^Z_ln>EzUQ+)`}?hAL@XROraw=QTNtu1Zkjm{2jxG~?8kKOaz@9T;yJSa)-Q_mF$ zT`dqmsmhGy;vS)MkwYJzOx!5d-y!$`{wn`>?Bk`X3i=l8W6d9bwa;zluo8ZCfYE2X z_63$cyP~Ae82jb8qSv{A6oCQJSl%+gKDYAfM+=qa7{qRV64CRW%IRkfaLqyucW zIm4ZHwuLMW!(4QtFI%@0?H^j)ga64g)1M1S+&WBWY5Ik0C2{P@B>1X|?j$k(n89>0 z+(2GsD-3ODU^vGBZAyO*IqPQkaI2#0$Mc}zclj@$z2EcoGhdTvoix4Ra~WJu&+xCd z4lE7-?L#(oawi@oi0W>{sx!&&=&Z*(Zqn^QFML#Y57>cH;?>XXP8*KGWr(ozPbBcW! zKyhg<4!v3o0Dft1WbC;vH1to=z_~JsF9SsPH&92t45FA12>$f8Q3k-0mRsdJU;nL) ze}BZ-<=&SWwH1?Ojs+r0p$jA91BJksp=uutvtO0bea~e=|BQXwn`_yM*EqUUhO&0* z@~jJdNL_`oF3evU zZ$@#-q0y1D$=V>1G>XXnws!f;c=fInF?kK3UqS)MU@|}X!6wG;kdjp-0J%LtG7pd$ zq3}1Ag74-?JbmN8X))n#p4()|`=u%=QeV*n0FXG-i zs;RE~_rENpjx)|U_wGLwNcLWPuQJzMbA9G_-@sPczFT+@1D@-V=hPGo zTW{i=9iagmGXmzjySo5cBrP>{w70KsnnH1NRzEZOA2oA#gVfvu?OR_)r8`a6ic@Etp9u>R#KHH21zB|v1RdnTx5bV%iP;u4tc^~ZhWsY69&>y zpzLm>SdV%?m{*E~GA1~XYwjoa1GW)^b4ox?T*lW#Exsd1jtI%hHV=ecz5#GX0s?w` zvEP6m))}XCW7i1O%+e?E2Edc3mP%+NUv4ddlnS! zd2wNWPipSNTZ6aqp>yAcNg#f@p5t7VCbOKN=WV4+!J*Qc1)}tG}xP zcWPd|dQig!9H;dqUMwI; zK!luF4Xy2vX}k(*aIP+t4^=MWKPF*>2_)H~=GxUYZ8`6I$Uc`>)`{JknI8OW zCY!=eqqBbA-&<{V6SB9U#ss52U=TE;RYRBoAna+~+l9cc{m~yB)B2?3A`o;Q z5Dvom0vy25rP|8vqYcpEg{AlB)_eN&k=boq)0{7W8Qy(ub8}PZW?@W`Rjp2LZZg1h z-T!3rCYXGFqJI5fSXZ+MVq1j151Is0bK<} zZs`N(LQ6by7|eX6M*Hms3f3nj2s;W4h)O51UR(@yh(okPtZotaawFrEcl7q z#z{7=U4ggBlUq=~Tsr_B1KRbw_JBRsJU%5Q3+QS`jvXsW0H0_AfV($8TK@qt*(A0_ zc!FesuJMV*xIphtys&8k9;~n0cm${~<8R*tfui35!;n=B3s74Cix&r6P1v+Z=<4K) zLX)>brbSwxOaREy^A8wTm%VyHr345f?LX86AZX+ajAKJMyaqVc4r|_~)om}8Y$ugG zJv%E5;AKY+AO8CE_NB1Q!$0Z|hadYVG+@~M-3IX0{)I#Wf$@K}O@79t7yt+EH`K4= z|AkWhpYT@qw#M;guyKU;Ki2}<{$P#We-fk_bN?Ge?Eh5O|A^B5*Ao+-=x67Hm*%8e z?Eigh(|WQegN7YPG{v#Yxs&0yVW1c|DREQ$gb~nse~vi0>6_c}IYzk!iA(ie1ETjL zT`}+$^?yDF48W0Jt7m)Xx=W2HWBtYu&6b%b1obKF8dJHzd+5d=5`kk!E9=ML_JOa? z(!&rX^*ukE{dhri-6RAz^s-vfUJVJg zIu-bfjM}YyFtRmgOnO8NylzBwr*?Et@^xmIM!DC#Egu|{-t9XF-ky*drO3>PpC%A{ zbh!d-G*c}ni}X36$K^A5qh^;Cq=6*5g84JEUfV_B(LA{2Njgz7aE%@7neuXF6E$1CLVMe7qcL7eNlrqE?;vP{}SPjcxVr|G@Lh3gKv z8k14jS6)My$|cw7O$F4fi*eoB=Kw3bMg!^CefA>QQcn-*G4jmxseB(h;{z1Sym*FU z7k`^IPFxJt`;7X&ENwKuqDCkH>y=oM&TKR_T&C(mC+1=*9QUY6V0e1@S|j_v(!zl| z%(x+#KZz;#S`lp~z9}}vj}eX0MKwoIuKTUjxG9cx-_@#gio?mo7qC@^$x?jo&DJSI zYlhf!Ob@tCZa1KeLsUZK^3Cc!TV&)M=#gHEdyTG=kvMK0%Wr~r6LN5JKKPYsQ;Ora zl`YzupOehh-&?!;{Z#Ko-0-sLPzDbz4AD0?#z9nS*TlOKwyHXvqCWh5=tL5v?<~gG znZ0Z>?jUFC*lD8{M^cPy?mWhh+(!mJWPQ9IFjZb0kOdix|%e&~(w~TgccxLN87W8KeS>^MeC$RM+QMRd%1;Oh$Ys_gwm46#FUd?LO zATnuGW2Lr~&dmqu=mNnU@mTUA^5}#ywy+6`|wxUdGq4|rt zMux;VyeZq-i=C}1T{~QHvL$)HE`A@bioa0!OPp?@fSrtUpNuJ~-hRVTRDCEXURv-0 zR2!ry#*iS!c<<#TE_hFSX0nn|K)B%BvzIk7TjC39nwIL>}%Y#%IOf72+Xo&Na$hkp1+4|145r5UlT1mt1NE(z> z8`P@>ttmCW&*`m?Vvg}Pxpi*52Rucp*{w~eE7tlxDWjtQ`oHI309Hcae_iW=Hj&#n zF;!|&dUw?2m*Y|#bb|}V!+X^zAsKZm31YTTRUQY4bKlTw(lpWBDTu2}(Z^1i#tjc} zYyY_1x@E}_uI3`udvM?v!k<{;TlxDp?$GD+58;uxa?kxaqsn*%TJuz!=szwK33gZ2 z^pt7MFd^q+xB`Ap95GUJ>F1P*yCj3{nM)jdjkn1Xm%}D5Fa^H=J={Zv!lDg!LOYBa z9*a}0{Ci~%Gu?0@yxRXmS6H|-Y#N!t_%(?KujcuOG{ksI-jTI$phJy?cZ%ZQ(rc1T z+;*+N-2{E--NCmVamwSG;B;WPGZdcg>@TlVXq*#4XC5#srYW${6LpisF`}j_)We79Qs28d3~Z|=}Wv(bSt%{qrY)83T@Gm zV)LQZi(_%$DCTmnIe;hq=}229CXI}C8T7@K3s|1NwcP@FP_Mp7RCY@6roHK_(Dqo} zzEJj&x3G6zHfmX_JULG+x~GUs9_#9Dz{CV!d^CI z--;K&oTc6DD}1adG3{ro>*TGXsq`8flvtr9EPaXbR%fur_1BopC=|Atq?meyWAtpe z>hf|}TmF(1vcwnhFTC=zxPw1o{7%@C)yR>nYiaB8^BVW7s0A4Z#LynX7HL0AQ#Ijr zulJ`$1?+VgM1L5`FFXoN*dW99{xeCdeMcB-|Yu`Yd zVefFKqnH26;P$GR|5Ac87yrKUIl})dwdS{@8u$0K@HYPFPO%M`I$;#)r7N2qN=oc; z0k){bSv)M1>A=*QGv&v8z7UxwR~?17=<-G(Hm-BtAmgK^5J+MNdPoXfC5M+qHQCsa z$)kX~{kBukIxKuO8CMPGNMWBOELtM`l~< zm)vo#zkU50H%o%#v*P-wz@*CAV?gSq=7XsmBC)SFOa2b<$lXjx|8;)OI9MyKmrpuIPuS zWTKb)wB*wqmWU-9?d1(KCWq3#Z$h8)fr*q4al-%9skAr0>ir>V{oTO}lIc zpP?bJgHE~MfzUpV`}QVKIBBUH$h{2&$7<7`Pkt7YFy zMdh}Mc@5j4gvAodh3?{P?zHW2J~~Rc%SSGZ-YS?**NTey!M65oIH~q&87M3LNRJ_U=Wa=o`2-f2&F`#855uuJ6pRmae}FWj0lIoE)eYa7^~#Rggy8R2l4IVXLLtg#5y= zQ;d?_#BmPK_yV*T&6xCA9NThf24paFglwZ|_;TrT7X60;{cB8Kz}wJ*wpbpy7d9Xb zqwOtI$pVH%^Nng2?Qncyn*M_PW>X>4UD_wn$6ZHV%4V5ZLuB(6!Wnu2!z?{(sM8O~ zTadZp&kH>jS-n332_I_L%zB*O;MYc52VMdf?1Xont?V+oFuPX1=n|A>?}=k2a&+h< zdcXnPnFW5!oFNv5TnqlpAy=5Zz%`sPdu%lOTk`5N<)6kQLM-HNHQNh&g=<(j@I>)v zjsZcVm0{)nfu*1xwx%^;a_-)(zkJ+?ap0w-_d_=<@+Q|{y>;#74`}r;;DXM@a`H>9 zaGyb*(_B&yQ`b_~QcT<9{3IMr3WcCN8JUOVge>3CNK$hwiSES$1i&mqZg&WOF%Z~Jj7&>*Ekgc~FJQrW1^@fCfOEcw}kj~d(^RD>nQ!57Ka<{(r!6K)<1#P)Ssoxo|y}u}*xPu2cWazev zi-qsnBk+FN@B*i>_eoeO9jBHF#BsDQgUgC6ZgGndM0{i0K>D)nille94qO?9^dff3($I!qiK?mbm4PuqjUEsQIIcwBV9Ui~ZP$A#$rVqRKt*4RWy z)a)qAs;w0fzlaGSoEuZU2x`D(mU#6_crS6djFuSJ61pldVM3rcsPad~CRLtKdv4%n zkIPvou9U!Jt;Z)7N###frHy@1Na23jX;2cO-Pf4Gj?C1Afr+cWaF)!e7 z>Qqkw>GscW0CVu6QW+)YBR?kYkoz;y816i9yBU?shZ#o%*oc)YcU8(`-SmS(>{}K# z5vbBUQbIX|aJ(a2+t4xHcL*w=E_}Y70P#8x9!*OPPc*O^uDBpYg~eV%%^uV4tM~%u zsB+9_b=!J)a;BZ|V)~X`-L4?E_XzsqG7=_Bq31YuBzdVYUo7NhjCQ)T>Tr!os9C8l zHkq>|qlv&7wmw5Vl|ci$8(7)0GGm`vN2)}m+-=ocowH`kB`~s$(_XOJha<&r;4MpLAyV_oEukHXeBP`2JL~B42L}(k(!1iHhII?4-QrXJCx4;q`QdGwOexA zE(Gd`k7IuQ9I@gavniJxa#&AiHlpk5t?P5`|9AG4F7X`mBzf)!n)8ZfX zN+WcT$P_UzfE&%lXULEz*Z?vqLcNm+wln&Zwyd)!$Qr=^&a2A|wT;HZX1 z#k@ooy;y!F+DFx@;YZa9#h5BLNDt`mByhI(R$rW1mAvA+`8U5d5GJ< z2IQ3PpC!4(zT|s0fAqPoQ^!b3zGSDFw6;ymjE;duua2ELmgfD~O^7RqPH_IZqIOMZv);ke*z|N)`ul(+$8kLtjz`M*=-O0e>5(9*b;1jb zLqXUi$6<-fg+U_}f#lAvht1&6ZA{2Co)e5YBz63uaguhbVxc!#|=!b0GMu@=YAFm!11F+R=&xz-Pdj?q7O zT*5Ec*2o{k#D)(}o^O;nvQBIYWqR;FwZC~2>i?oszTUZyaAs&B*gi51!S3f-pf0)y z*eEM3#YnriD@k%i%>^sAVex44G>ZwtnKF2wxllW<-{$%r*q=Ryt*AgJwAhv2HN+?A zzy1jN4W7}^cpEtzv@mHXD5 zV<^q=x^%c4Y!|ero7RQ=C|W^+sn`@L2?`pV_A8te63j{7qP@>yF{i8TirtNVe8`d~ zM&Nb%t3%;twOU^JI(-EP6#MJd;cr?8A~}NlKJUsYCn;!?et7!sSwM{(Ku(%zeQ{6HehX>^TZHUW195T(Cb_7~+mdp1 z!#E`Eg^9ve3W3dIcep0eBG=>U;k)aE^?ftzlo_fnqnh6Sumdgw!S!#9(`s8Vmxi^d zoE#Ok^mpU?FO@NxZk@+M%o4MWuN|`K(p5m_Y z)<#6-J*jmaHl4@C#;~Dg8>i?jmO5RJ97ke86=gwX z=Y!B6ZJm%~$mq~9yW_*W&8>suv!Xt7$`4}7wj-b>vta5+XH087OMh~xw2c3Yq1o+0&I2|7U4gj$IV%c&{ z;~ywi{FE2rm^ftyOh`l9%}UZ1DotZ>mmLmmSdl5*GtuOylLq113}XKuJoMbLg9ERoUE9@9*gm?Y)N)k% zfC%1a;1NrQ9m#Uo>qGa%!J{va7?`HwQeqiWUtN$B-Cy&|XS#LJ$&A->M2)^(JO*q; zms(i;(e<5TCk(z9Fj`R*X_Ax^yj;VKnRY?hr?Xt4K?3|*s(f4zvb`HtjLnv4*5b+( zOcpH|n;0~&w8iLL9!6LV6P=o03?>EI&^}279gJj={W`7~8@thbUDDYjMU}#l&#SzM ze`5>GR(=ryxX~~#Ru{EHuKv#X+Yu~|gYv_qS()UjJ3u0^Iw7;EcGF#^VP@ zEu%a6?Tr;pn4zJKP8wxZNmedzDuuY#BD+5^Qt^m4HE{f{F)q|BKIV|}h82I*bX{|V zh;#7`&;cZI=Zwe(ET5HPmVfUhS66zKnPDErpix97zdPZjWPopk&Z12|amy$+N~Eut zpFC?IlZj-zB8ZL;ZBIlKJqJVO#yMK!ni+vzvTPbqZ|EDib&RrQTMgq}hRc6u z>VZn&Zq>G_3s0zJ^%izE!_PjCdT`~I@O+-y^v{^d5;S3{QI_!^7hl?Vx9hI_HjQcc0_uD(-Q46 zp{FCFFA3lI*A1h1A=!H8VIA^in*KhY*d@?hoCTaE_Ag@(S zC3bAlq-|}hvgG>Xy;a5o(-j@tP{FT1n@dzS+;AGEDI9ib&afVSaD;8HX8PtNNp}&V zOTUy+f3;=orWN_z}? zzQxw->|aHqhBJdfX}r6IR;>|-QVz0>%?sjWc~yIlG;hafK03YM!BqtB>_dMV_3eiP zWUDbr6&wZnYag6OhgfJXR3H|+!5z6Xbg9(4U!{_{5^hyn-55J67S*$s{3Gt&yNnIR zHVN1#&7m0%vs1k=QpaP!(DyyiHe*YLcBa>5>IWPJ7m075Bx9a;UNVO4Se;w)F%k|^ z1_fQBhCT_@)O!+>Q+}5>MIi(FmJdOBYxdaZPIG$+=FKHaVO*z{VIvJgMT^Ih`t@DB z%R1+SpAg_D=&@Y*_fQhDK2vz_pu}>|B(rgr5{JCCa9!cs7p-@(4nf%iv^Td9YeZS;_A)gn3x!&AyU11E22XQ@o~jk!mAfo#~(xy(T`tc-I%Y$U|SU1I{P3B2xAG70NC9Z$Q z=ak!VFqGRAC|)3^R=_b8YQ6WhYvA1&+&Rv6F&3pjxbk6<=tIo9m4y8^_%6eW6lC(^ zgNG?uS;g`>tTBpZD9I(mn~KDtozTwTq<@*X+)j~`LFYs-?)C!1&i%U6F}AWtc`Zt^ zWA)np#=RIQ!Iia8YEY?CYnRTQl)3&J>uczB)R_p`UW^+_hc4u)^skPlZu ziB}d@*{>xw=mbsjR&BXU_7+PrA~048th~r|ANy2OJ6-Z2?cz`S6vGz;g+Wtoi@S){ zVo=g;XcFHk5-QInUr-${vE1APeln7KMXtR&2_cgEg(ayM7nua(L7Xtmcl!S7JrF~Q z1R@0ZzgqBkTQ_ACtiE@*GOPfKs^^eYR!qg}FQ2 zfB=1KbDNI=Wz37oT1#UB8Fwko8q61qu8u~gs*6Z^-_-=Y*>e1LloC(hj9Y_}@>=sM z;=EP7C{YF<9>+@9H9mkk#WQeMLEzojd6gCzeez|7?09WSkA{&X%gzKli+87adh+&| zxGWNtd2UPC=#9B}#ar{!7u}TAhXtt5w3C#Yt+khc#+tc_U;oO)yDMR+m~)s66DWSdvbx^Z^=Y_;ESKizY z#(gadZKC$9UfOC6jshFBDV{lD{Os<1mej5KCp1i7TWW+8sNiTld-lC-z&ftEZPjOj zC7m-Xir{zAmYI-Sb4qHYF}O)IHNxmw)v)hos8F4Y+m7uL2YG0vKGa?W0l6@wFBa5b zXj3oqF!lN=4AvwEuJsWO?JFb5h!#i%apiNnkc9fl9EV@sFIbxcoD#oCZZBob6XpB_ zj@#G<^AOGZ755>{tH$PK3}n*!<@|feyXYf|krc>h$xF5B43m!TPZjtaTV+TG@SPRv z0V|g1ep@f1`6rIT#n%`r4!auP6T@_vPbJDM3Eci1G@tqiihJsqg4t|}kk-WqxsrN4dDj-iV z(68LY-%llT$9}F&q-Q*LrInotpI5UUgI*pnAT!upS_ZPqw= zo&la<7dLkX>UuHbiy~+Flg%y-XGzPq6__5|j)-h7|Lf}mNVK^${R}kf-C3G*clb1l z&Fi*h>isT4^2<@efiB7bUY<;pvZO&s8`XNart5I>a4NimdMDVO`fL?AZP_PQOu^#|_xo?0rtjx0GBNR{{*N+ZRUtnv zuo!)iJ=Xse9Py;3CdBk>{8|kbuB1*<_Zkhhi2Mht17!OCl?nMtJ1);kp7jB_W$rlQE>vNRqk z!&+X+#MM{u;ufJ_UsZX3ZGVse{QOxd2muJ84AqrwDamOY=IQiy*Jk&ZH(dk9L{{d?*6&Sa z5PNOcfaRB?PCcQH&8}XMFF84nU3huI94BI4(#C)B-xvje2#=I5@5{=O zM{uS9-YyAakB)ohvgx|7l4WK=vr1T~z<PFi!*=?NdDRoEAlaHq~n?dG#@&4Ff1{#`3^N1D&?%Do)F$0OQx zemjaw@#e61SLHP(YaeROjUeMfQN+>}5xDESr9!xsavh__+6wvr$)3AX#-pEl!~)BI z4+*#Mk@foVhRms%kUg}z%MAsj%g#{{{9M*3QyTz+?_?mFCr-f2(kD;wwhpwcDmNGk zj|Z6YI7fW|WAU$N?IEuipK4Mi%2%%ff^!b$J^~~r%`&o(ut%GL zBE7|&{H-vv$Tn@Q;8D@Q4=G;JJ)3*5Kvi-6ZA{vtQIkg?1w4(JUp?&=bo<{=m}>>d zt+lult&E;>663m-V9iTq^h${BqEO&zYH#Y}-b_s1{Cu=DWSTP;J5&5~TtXvyL40T3 zuf_u3I5);MU-y6Flk);VJjEx{RZO8E6DN22PJWD<^+d;bpcykHGtG7+)Os*GB)YDsQpp-Q%WZ~I6f|gV(dWpz%F!=+SFTRq zHa{>AiWB_?6mWe_n7S_7!Q@N+r2RGDjxn!M zHHWXp8feaqjneH&c@+CF7)| z%}J@IG9XzIlYf8I{Fn)1f;rP_Tg^LvjZ`Da#&JVX#Oi`c3ZRNOA12Mwxd!dnYjg=Z zEiW0goxRmp?3C27nPMcYVY^E|u%9CE&yoAEB&@;n@TX3V@q({XMf0ht=S8fGZcU}H zISP9!#Y1OL>XPCwq!E&N0t@ltwTLKQYslEgY>^l?uLnUn4Ai~ot2MQckAT?wtA=+t z`p{{tpcpCeJZ3hhM*o)t5#1%~`v`Vu)i1^fhvu%Q!0_*L7AheV*Xc;ok{5UW72q{f z0UvTR&U0*jJEbUJnx5uAX_GX7DD_^+AfVkj&4o3@Zu(oQ9gXfLVPK{wdFK0<#IQQa z>!cn)rjA^AYji=_$iT6wlVelQL9#U65!Tg-mtIqMd_elHp`4y0VXOSlvjPMfCGS;( zPN!hHn+X&XmH}UY?~Co^C)Y^PhzPRHLUmYLce_Z`uFd&5o|96f=yr}8zn@d^>5x{c zBjbAwj1P60{sD7(K2FuBq)~RK)H53uFu&(#rRie>r0-b6S3~lt-#@z6ba?6vnvTR1 zw0!k3ffvN^k-hI)d5GD#1&#aCB>Xq=(LCP-Z_j8}V{)!TI$Xs^dbtll)}*dSD8~HJ z0@6q55Ra`d0<}LlSt-0dB)c=#Pra!M+V-XsoaQTbgHZZwN=%uylPAKx_Z z)JQ{Ao1VidA4^6>E4}`hOTC)7vgOLi7Mz6P$=O<$SUBgg%;WLt6C6z6qrVK;CH?)0 zn*bnnTGZisB)a9!=3i^84{#TV+9xZV@k#p7+%FFUBs^Z5OM6^y0L9twJRLzjUAVR9 zllpPIW5AM+J;;&ecQsvU_-?i#3<40b`Cl~q6n|jRPJGQ@ItQ8FnL3!vm)sF&H*~5> zC>%#xk(3E-J3^Q4Pe?-Yu}&?luy6GO1NH7#Hq3qj z)2gzkf=0=B`Pxn)fx?8H#{kL?H9ivEw-cqmpQ$jJGZ}?^?B@sY{9~vv#i&U$Dj4&E zOg0HfpGcbFCH2#XP}2_n60v@@PgU$ z_)M*}F`#xDPD}#jJIavcYZA2P{Qhj;s>WoW{wA*$#M>KO3_V2-slHc~)R(#oI3=^b zEAou{xQ$S_gj3$uxrT1PfF!4WFt9_86lF3aWkCa@&6 zic+)Z6PlTR4jw8_T#$}17)Nl%y>p_3@3$?Z4jG-YqrC+t(C)SddYFs%RNHW318Et@Ia|+h8``{AjRtPdQvdjhr0G`UjY#p@O zwXxY2kAPJ6L9^Le_b=#Q)Tejtv&*f!H41~{M1555buaj{h&MJ8f1|D`RC;wxOEzn@ z!&%aZx5EPJ)8iPVKcnUQdr$7&8WP%pyAYGziJaIc07Kj2*$D`-07bPa{`zb@K=~FN zR zGgk|ovsoXT%})B(d6*AqhkLAx3{o~UVWQ+EjG%gFxMLGEaU;q^{>1?I(1K%f=C`+W z`_zP|rwKM6Gd~&{eBbR-q@&jT2v~7zhp<8q+K#7^vOI-|1-gGfFR4Z+CEEv9qkNL- z%D5a$cXzi;2bbLM6;g@v=_jyb3%H$qRH9*ia@qo$k)HF~W6QJEq@xCveU!B*?>Z*3 zyWpO(@aN^jwf3Xg`lyA4X&&0ba-M^cE%h0TrTZ1gCpR02#arK@ifLQL^Jntud^ZV! zpq01$PK^tC+BOY_3|e9T&m@Ef5YzLIfY&`UQ2XGDpB&|ZbnkVIjfUNWj&1VnO`J;- z;6U{V{UvopBFpJ4$WRmJk#B;0^T4rmCDWx)w;w%inrWi#SeRgBD73_3@lqEu6q%&0 z@fQa=R4X@<&q<=D3o(K4lEbU8yfn*%Sno`nvSpX4f=*P(6+Gj6;E~ za!S>!4D;KzOld9SzTc1sL{T(e|Q0P2=B&;mRcJX#I!@tw5 zq*tB&1_W|ETEgPP~AITJ^KwM!$)1Bobq?IA`iM!7;^qCIM6v(N9yW@ zX}Q9>aNDJ{xPs?MQK^IZF|?u}ec!<0FbM7SbL7~p;s^2TTWa{U`J#89)x#jQvH1$E z+{L%Hz=cNU6NAwe^$3FyQsCNMn-nynB>!p9kYo{J%4-!@bj*9ZydRS;DSU~k8(hgZ zH(P6MVDF45;1rI0UH5Bq2 zglh1Eji4Z;yoE5rgeSlnK%?*TONNKssnmT8|8C!{4 zc?RrCx4}&YAChMt7?he)^z3GI3_q@PCRC{C`yPpWk&mwyj!VBy^h+x>;3|!P6ski? zPsZ1E9zU#U@I}Hd@8awvuaSPq>fx@@@OYfjcP%fpT|rCU#XGb-!sB&MWAbv!ANRTY zJ@nIrn@_V2EdBt@KBVSl(aS0A>DN>2c}g4;!Zp?OdmX|F&XF3YG-q`J9N`X!!*`B= zqWc$87?EXKrC#IFLKXSM2YcgB7r>|o+H)!TV2Uh=ykO*U}; zj&Yo^TNBhwS=GTNoL`{|Z0!PS!(HOzb)xLTS_d`rY zTJAqD?rh`p?fQn{kCO$1O(|QGuaJkBIkr3?IW3|AAY~q2@}Ok_`1&K?aa|av-SbG( zD`x3d*CcNdi;%tKt<464V@HmIooP;J^MlXh+gQ4P(d`!NvvDZ~1>0E~@n879<>}pP z=AgvqloV+VqO#Q4bMsWd;iyxkv)JDF10<8e^4IJfGeX40hmddESa&1j4L%dKLb7MP zrxc~jFrnHh@?}ZbcUJJjrv*OQKSjC7RzqenEq>qCYprF~`P@}Ve>_WXXRg0DNW)_d zCPy|>tWFx9n8O93d#gXMf-+%SA3%lkck z(@%x6J5|?sR>AC>M_;}`?l+jA6r%dnn6StJoMBj$H+`(@)iHLI5DpUcgWFesEnHU+ zRw~SNd2U5Am@Cq?Fqt?*fd=i28;jzJ9wfZJ0M?BnZ>EZpWMdY7UpekDWntq{?`xvezce zhbWuPI=0tKs9Agm8|VKuI;Q-^5t_hW#29bh>Vd=qnr^R<<3aje(~_M=@xpH<{X#UJ zT;l~*cub{I3|l04{9VB$FvOH`%mA`)nG#in7*qh_!P70 zr)MT?)wO}{N$R(YiJoHB)QU;y>`6L*jGPLYr=~n^J!&@{Y3dn!z})MEqeE~2660`2 z8q0d`60_U7<&)oykYRgmgH%dncbAAiPy5gT7R4&aT82qq;ti+Q7x*TB$9Gw!4J39z zIz?$N`Dv0AdBMB=yHywtL$OH*WFiiiX?4v~6HF~do0S$sqf;yW-_rxt`@z{5m&zhS ziviG1YcktBHiqrFt1EmgKK1uQN6H zaCqn0^fRh*m8va8*96w(w!pYWIFZz6^VljpUh%rw3Vm2YYV9<0V!1}p!)KVBM}rCV zIq6Y_`ZVBDBRrTAWrTjQH4KbL`Irq93Q@4vc^>U*6rJ?KS)yC1l+F7s%?rjhK_C{E4`?fuF5YZ7w;%*Ic+Tg$6w)X$YP!_ynB{v8 zG$`fz^bEx06}GG0*PnOoJJz!td)m~dr1u&#@B$HxG?VwKe^*@q4l8(td~K~VGxN@c zh0gij+_%ikF>=s__O}->sBPe9QLCDQlhE1zZg;EQj^j)pU+sH-#0flQro279E75SXxGd-o#glE)5<5|As7Gt=NoYdEuB;{-7PWNt? z>ZO`}+Z4}YPpi6v4{VjP*QH<9g}RnD_Py0^UC94g9(^0g5mnr&?;@tT$|hu;YJ*)e zw=KMrZAao``K*AMxQ0uJ`qcUC9N=Zl5K z)itb5i;!G>Y%@O2Osn=nK`CCfqbG9 zPA#G2brSPcU3!F!Ww+ytX-o8micW(83xh@VadYiBR6w4_`I@0NtiHI&ovA#Ue$h`C z@puXcxZ2)W_C((A2oO^+jXbme%m(>7hXbEO5Qh1kNKC_1m!rY<)dw4P$84;ous!S> zZ!c!QxpmCg;3}y3yY&KdgoXj&-0iO|BpH>zJVAx7OLg%%#r?+LBUU1gYg#S&(kOE` zc>QWPoW4C26NHT3Ar^nRaW6i8;T<%BdikZqY1dcs9e>|nj9nI@;t7xkL{-`m?uWW0qvle{bZ-` z&+DYScSqMhkB5e~#lepj9h!AG&162a%eXJGSx3I1sx=f35*uno8h{pHu3^w1*cfCHp9+kp303C9!HH3J0mT3Iw#ywEKier~#R zrzkeBCawC9P?I;)zA^kHdG6df^mu)pnQ@fo4sE(4{LPyR*6O3b945qkN4z2|;bvzS z?eJ-om+7FfUWSnQ;=$di)HX!_n^8!Ld*wj&T(bDJ5{C;arcStBs5d|C+J1@QC zIxF69bg<^3lQ^^0A&bm6SgzEZpQU#2#+{qvPtf1cD?OrvrlM=#EoliEKjU70H=iF2 z3z)v`+dO9_aNu|7DVQ~M-<;05JIliC5<0y<@UcceAaHo`A+Fw(wBfY$+62 zbUi3VFt$dZsf?n3B&iBg?L3zJCm2UI=~leX<610beR4ci$*c=A{?x^_q-MqflOLq%N0oG4HRN!o^xTW)PC7o%u&QG6dWUV@b~I5Ff_3&6=ycvbVcaw<_;W>_}N5 z#=7a%`0xJW`&m#~PCI}{*mZuXwNNbGj?j`~6&PlIYZA_Q7LB*PWZ39bPrAm%)sxI| zb&t(L?Bea5b8UXmQ(>#fh&_mXuCvQ!^#1yDbmmI-)7nA$P^}vEHrb0cT*qHBQ4-t~ zgwxHQuYmhngrZ)I7he9+HzQ9VmD{Ore*jb^A zD@I)iri2(=#hey0q!(~DRI^I=ZQqx?<2EdQN-8^F$Zmww|D^;$Ryu>@CT)ycf`>)58Hl4|JZ)h@qQM@oMEn zbSSIT7ZYGd=v;1ja8k2WWJXNle!2F@@J$zD$GG#jiiV@__1e%i&$f|!akJ)o&;rX% z`Sh)2p`TGi<^;PkL^HNy-`Z`#Rx21=@0AAw#nlv??F$-KYqfGU7AU0Un=%YO{%& z9itHA>19*TUhOQfT92~FlNWmX8+NEBq0Q@0#bL|W2lp_Coj_31nuK;=y4CV0DJHcj zzI42QvnXYjAAMUw33qN@3Pf*}Qc7d@ODT^~Mf5T-28W!pL)Yd4Ry-pBO|uNBXkE#( zH27s4nVTV4Jb^uLet+b2T8o4Yi^}Wj+ilFmt>`hSoNvh+?>)qt8`i`V)JM^$ zP{QqvWYB{@fG8kldFfX?LF>N`7lW6d#@4RhriJ*ou16UD2>?_Jy|$%2qFGzdOjwm{ zBMF|^VBJGO?o-3#Y-e-#<}>V`;$i8M=d0=Ho2S?EizS=&$Y8m-7vzGe!Ov$xnY@LM^m|4YMYv1anIymKb zj~+h--dN3aH2Pvd@4Liu8N81ibR$JXLIL8O~Ut%p$$kjo^XIXbWlkziX z`A)kSybc#D(Uge{F)L_ta)flqPXBXcpuosbMKiori&lq;B9w>LjarS_jmGhQargzE z6lF~pd>Ojz0nJlR!U|8$T``D%*oi%g6RhsFwy$VSq7S@p!L*fOSRkRlgg^H z&e^StsG=(B60QuJpFkKM&5N_S96shq=Qp8-9xKq&VOy&_wd-pE6*lbjB86q*i`rLG z0R2x>R*DmwH}I(&^6XRfmr*TP&y}qf86SrE zluI%j$<%R@?R4yGO?m3Whj=w9lWIMDef!F_mwxqkOQzN4VeX{Q%of;h9|NzT&Omwn zQ)CwSj|Li4yPHzp6~%WPNc@!{&*O_Nq*6elWoAYn<_|S$szi*jL5rVGf#fgP)TsE# ztIb<8-;c=>e-CBNuL+cpu8mHHnpIZZ$;#-}TE3>nehIQZE&37(>{rqgc^zyt8!i>y z^!L@yTSRfQU9C|H3!uD-nKSNz^44254{|bXzeRJ0W|Z!OW%R1Zs1{w-k^sjUshLt_ zw(LoeVH>m_Q>W(mNmiI6MNfFWDF3yL%SVP=nj8&F_sVCWTxG6gWd!3U56wPQ=(1p0 zK>5#%s@~L(*@$1jOEHZH1tf+F2|8z~Io=9-%rRLj?0K!yP))?f?O7NAxu7&Q)js|#Rfw^Y6l+G2NBg{MU$J_BiISYd9J)%OfQO-LRhh5}l>_wtnFGwu z>$K>BF6`E`JH$i%GeOT*!0u@kuB7Bq)6xU6_<*C*!?UIwxjJ zB-9D4JV`Bo)?Leo>{S;qXfY!awyN@(nNA6M*WwoX5rxf|Es+Uz%E@Fc7-Xors^Z!< zR*e=DGud>oUnMR=+q%*~A0${X)Qom!Gwdo3u6*tnkhrX+lZhjuTLR+3g*_z$EFVIF zjd#7=o=czNM@$Lbp& zB~D)rwKd{6zh@H@(qPv*>RF=@Jcupo8QPtl6e|Lf)Do0)eqM7x(#-0e?i6M`j-=7< z14Kk4XP#a@o+(^5s!~{@cOc3ykc0DUc65=-e}RZ{`Ca{&1W@?=GQSR+tO#3@a8jDF z)MwS2V}b|uGc<3i2SIhz|3$6E0d;KUp1?ot6RFdntPCOX)yX4 zl#}u~9oKf~JT7_!S)F@!`jTwyAAa}3D9dut9w1Z7SWrM)Ar>bGA^F}Pr6b+eNA zRhcm!INq`WqB;+z*z5=enmxzjDq~1Pa@a%G9tDus3HA|gs;fUd!-(52Z2yZ8ya?&T zzLvs!-r&C1`$X^`H2EQ2*Qvl>mqTs;o(%4{F?{&VK3OIm!WY$BCXs`Kj#6j#nFX zn21Ha6@G_vsxn#__#0m&3C!44dGAR4pPzEF}l^<{ox2?rlhCUIR#7~uZOlN{HAYJbG;UdQU!Be1rOlIMyO1O)}lZMlZ7sjCAUMTGY^=$J$J zx#XnIp0&clgQ`kTPBG^@)A^1aa~H<%+@Drw&>`rspQxm8PP(q`7hajZV};R{8yyZ~ z*Sl*k0(TQ6i-u*_LdCMvFDYxi{bqJfMFwyCXX7CB+2j6;#yvpewphw~gEWV(Bsxi@ zZ#T$oF$!&Py!BYYio(LTAm<-QNDhle5@P>wbFQ!i*~ z?7evX?!+wQd4G0iii}Pn?j)Cu=e?5kQY-$Fa>K&P))IVEzrpErU*sccXtEw}++#Dd zXj}ijDz{;caI*!g?4!OClQ+SwdZHRQ)52F`d3{egMntyV1nC}OuW#Vvmh;U%;VKU2 z?mudCJ-})FSQ-Qt>d_flFb%OUZ_uqRTJf=PZSj{W?>ETKc#`^lrq% zvM9BVT)is_ACe`TCqmmUaAlwV^1b8xNT8L_raQj_r(U<`sD>5=NOoW90R5)64`-Qe zaMbTgPFf9+b#Z9j*FQ~Uz2%XAM?=4#2H5@o7Jv;~ZQnNggUI<`EI9W4^mmkx*iFq= zR)OU-}$dYjUH*!VTTg7K1tkpKPZ}hc4;eBYNCE()S?XD+f5u zOG?{6^TlUYf3B*%|ms*5zGkRO)E zY(t4Q;r<#qCbhatN7^jf_YO8)e#n7E&&ikHFSvCEqK8^OX-l4v_dHhM+KlUxBXS-} zYKM7FA=64obnxT!oCcbO={Tj+#@}uFrSHFBEUJueg z7Yc+Rs;;S$=OGKt$*QY_cP#f4J)qCZqJ6LI+GjoCG4sTF#^7B zTwaxTx&AEecTom3&!^kYcf-Pyf4Tl0kMe{0lo7^567A_(RTY%|QZPcXLUo7jcO1Xx zo?p`h?TFC02}EySyN)B?$JLX&BZnU;olwg|AN}~nEbD(ZO@m3V?eAUAkelg zVsJd^`BUX^AsrHE_b}0VVRTv(n4w>mIHbf=mA+6Va z3@jbGeXENeY2OkUTXrqX`YmmFMEk^0jIV4=3;Mq zI=S1$A8tFmjUIQ@1m`5mOIqo_94~6znVF1Bq_7tQGMf0L-1{%{_#)22%ZsqV#SI%) z@3n+IO^IyLTfnI zUQR71PFqnLUuCV=6%6jLq8|EHpmof$QbORFJR+Q$p=2MJKZ$CTz4P*+@yN1q!Tn8d&j79$=>2Y zzKNBK68akLVRFd_Jz_t8&(J*Y8{3beH{_+-H>_Z?qvPC!@}No6-Wg`iMCS9fwNGnc zu(&>#?DcXG|Iy^0+Y)!3>kd$EjwV+=!+awqur2wB-?G2ll2pWP;uQ8U6h5!9g0 zCxN#*H89!^WGL*#oMygn0owTv({)Njcgc;FYZs?pKh0{plOJ&ll7RboT-(_LM;l}6_F+IaQW zA=?dQKWGB5uH}Q?-4iSQDy3J+)geWs7fLa`n;(0XDRz10ervIs&EbQf`=KQ*9 z>e0KG5gyY%e*$ESt4OmA#+duKXW|UK{MKE@wF&N|ntqjHn%|y{ODWJyL+5$=2|lS# zJZ4XbeqcqY<7T<1j(|f~#_~X-spD>SmYuk#;iVVJk1THO@l)OLA)R7b1lZyf*^BOk znP08mY&jKOqHPT0-~dgq3dQ$7o+;EZ-3~Cqc=LcPTa2>u#7s47H7qtOnMsFSI1P<% z5tK?g@iH?4L)S}^v|(UQmy=Dc+;;2NnUyy7ZU7RTCvA;C^jOtH@=HW*Q^rBV`ZMmf z(i;?KIFxxjX3iFsvbkG7{gryWpRT|8-2jdNs}OTzua2@&nXlcXpl^5caH1*v8n9!w zu|=Y|?3xXQHnP#ZE!sj~(F)ej^w~Pd<%|tYt{D(sY~ErY;Y7pK=a6@nOoHisBBH^> ziDH&D<$!)n%f=@OmFk)+S&=%=sf|3AzdK!QqNQ8@=;iRH0LVPGU{9rx10-QI^<`nX z#@n3A2*DFB(iOqbz-b`md&kY=o3#P#V%yKjq?9@`T5knVz5*-gS`4WT`l7?tWV$hF zL!!eV(7(Ls6-^m3jlOlOM6!Gbyu^X=2Urj7`7A$BTLoi>ax)(X7E9inhVf+&C8r?A z<%+3|Pp4`{-sW5eJ7lpIHwIlkZB+HWgUi7hyyMoSOurKv%JH%Mo6X@|_G3OyXCccD zPxLAE=6|dMPeUQR*xc2t2?!kTH2taj@JrJdANn&M(mmga!IZPpi_-@Kb-M8b%TLA9 z<9xat0U^}Ps8_xjc@wz~$l`oiP0$(z_Gueq-xH7|^Rt(^vO!OtJRy35$I5{yBy{7O zGTNlZEc-19@NPWXa)F$!y=tSjBmMDh9$C=o zY9Ji1>?n`0xgoFcelvt{HX4v>3|u{_^+ngd==l`1ogo#-Npx zgcaJURaxO&r|^WUtKQBBFpnzVV0#Zh+VP;H@Pg!GYU-?n^hjFpZ(MmP5XPU{dQhjk zwV`T+q$In+!BuzSr|}yO4QPOlbzJK><=zVFP2?igSpxp>m15N(;^t@cZa<6r@wrX4 ziXB#p%CzJII=bIrZ53+W_7_K^VtG1Td*O4`D*MF%#6Fm);m+^-m-*FC&^WVJ&S|1H zvFcM5+NW)-ytV(cV9!RAYuw9Wt9DDfAyczK)6k*8l=#EQ1kF2u-i9*aV(csDLUmOu z$OXYx)|&#}P8_EHa&Z_&V-}<(w0scQt)Qm)D_2wWXy|k)eInnEz0icWQZZ(p3Y6;vk%Au|Mut61e194k|$3x z-TyRNj+rQ`*fK2W@TZ6|$+ypBaZroC zDlgDZtaw__J7%r!k0eRJ)gH;R6_){`WX8}~VSG^Lr_WMHIhN;zT|v@Y_#SKIq*1}A zvzhBuYCt3!7u)}iHQLW|y}+YddhM9r@OB6s7Ax_W0D{e8&c6E?hwO5{;K9717n7u| zn7T95+gNVv8I3wf$IpqyNj?}qoSdgL0Y?WX!kFKM#lI^a6-LAN9IY?+ZN`?Mj)7xj{Du?X{C77jPzvCJH76E z!jM;;&$&C-0f~VgxVQwqs+6_kq~fi?Hd#c*WK=M+2%2#m_*Dz9gM#`jS;>riTBTJu zO6+h({zHa>Is`<}Mg{yKdng``w*)FpyWI_cJL!BxG%I6v(nCi8}`{I}m5gB6X8A=R&-&I_(-o>FjNrwO@DlZTUWxBfoKK}kR zUy4peTw~|&WNGjjzz93-pM3S+T;@4PDOn{F-1e)}Vfmc6rB|PKS=kz(LiZn#SLtr( zvew+&F)e92?-@I-`*L1DjiZGM+WF1{h`yTire?`j*ptSG8wI@k_+iuf&*#kTn>pNC z1=^`uu;blMLm5Sv;318+UlhG$fE^?y#JOgb@o#azJ zNA*JdKWx6Iw|(C?seY+iVL4?&CQLznk-r|cmUkYI$w>W`q(UOF!l*gf?a$r*r&{xW zRIQ_U6nHA9;%nMIGnLf+sV`4RnOPlM2E@pn$=3GnKiaf=CH&6O1bJw$uFSOU=qx*N zzB%?(;m3s!9EkKMTrC&Fp``02I8mIAdEg${yi|(IK;u+p>d=p?&(8afn5F(FnDfmo zAez(8sk9ly0`^MMHajTKAyFyTte%==H#f+EIp6V=`YQtAW3Hj2<$G-)u}vERpw_hy z=aV7P5D$_+${$w?Xl1me;!Pd|c@_hU`ec@pwbI0#1I@Quqzp9L=QM`?IuzBLRewE7 z*Ot)mkz0Q}TmH%PR+Pv@Wpa}# zCcYZDp?b<-tt&^-^4M%mVL6yIAm}cmgZCgU6&!ahRk|kpICp2C_xb0^-QN2g%6t># zO(CP?tS0xuZ=*s#Ajr$LT}hS6A}gPyw1V~+c{G%s>~w&9d4=5YMx{7}?n@o`KH6_; zZg9j|HnONUXef2Sg>!3-uGRh9=p0#i`l+E+E& z3ZbZO+nmkBhD;}JO^)m>s&&g=a~|q&PD&+7K5}cEc5F65nMi! zeQkZNAliGMF`p1+-7+4Kmz0(=Ti@XHy z!;y767N0$vrY&b(WeA8lo;)EO_O*r-1a_Rn*ex6!xyDglsSs^f8(eL5WDy(?05Iw( z``1!^8S7FXGw!(oir9B6wYfm=>O0CNjuIPggm+3&1=D36h6@LGVsy;K0ZAT!3Dw{a z(wuhx0JfY}{tsFcA5OActE>Mqa|r@&_>>@U@<_=&2SDEvs=9Dehv;@pLtH}1bDGbo z%FNsh<{&;?S0lGvXC&sp0bB#A10~W6zPAm0I<7k)SFWlLwD_djeEi6Plsl|H@BaXV zB2qX|bp;nL6%fX5A8+WIoC#Ac`@^rgFI09|Uh*Ft#c|yMT(A-G+%}x|G0k23yO+z$ z_pUc@n0ymifHHEV=rT^OyShw!hMS?D`DLjGx|Tdj?QTuBXJjH?kyEi8DFUh1sF0qD zwcE_{1t5VU_ea&rkNd3XNngekat;eQ!_f z+;|^eSAmOt@-KNGaJ_H%KK}&gW~o%9-j3gwfxrGd{#BC*U>}~_sW4HJBxN5By_v4{ z(V~2g@)p_k^Ax~-zgIR}RNVG})WyPEX$cD){FK4d#)%-M zn^8I&=M-u&Rqkc7Z%GORN((j=z*XC&HGegHzv|Zo1+q9o2gC>Qrpt3H^9JNtn3?k5 zx)`7zg4CZs9Y{ZkWSzbfUvU}1XhFYyN;JtT$ONi5;sZ+tK5&nj6sU;f%$|Pb-Gnha zOsQiY*$e$NIq^|}wV3o41BH2o%g?~QpH)N%oBQ`wGby(!MZNb%kn_=InNS3ULuvgy zs_ojj*&V<9DcQ2L94dab*dmD+drUzl`PE%(ytL@hs9~@W zU0~Q{JNiNb6TsKKuU~b<_hbTyrN;x58C7DQ9K1I%UHxxL*Z;H>ID^VXI@$`JnEf1A z321wfi=gd6_qdAWVf&W@oWWm5&z=8Cg!<>lUQ;>jhx-sER31aRu@wr3e)-xC@H_J# z*q}(_SuUjC%8a2FKKpt^xh*M?dIea`1mC>PEf!34JF0k-*U~aJbo?-&D;_%dzqt#e ze-_*hJ*1x46r=vb0H88~zx?p&oQtvn{f~xEKkoBCik*Ls!G8_!|2*-(*H`~M^S}B^ zRM4X0Ld$3E3{|a?K5+@)N8nv_iyU^C%wIpS)gB0ZnFf;;J7-O?9+(leZQ zRoncW?CubAmTr6^=3domOTe2UBtXZ|H<@}Jw%Ea;JrieZ|9{O}m*r1ja~E=|Z>HbA zP+NyA+XYrfAUuHiRY7tW>#;VrIKdsa@mzkPRARhK2AaOl*ks>L;JI__4tN=M&!Om2^d5)Bc^N3HPl7)czGTN4TxU(~>@Whjxg z0mzAcJE0De5xN9* zB26TUy85zD=qph&sEHfRSM2ZPjjVJ#tw`tHI+oQd$~Zn7S0?h}jP#IAF)NrT!5~vt zVlnWrwn9+ec&fJP)`iXw2BBVQJ_zdIxF`a1dz>5U&%+)Hs0kYO9EH<1$8W?y=l@<9 z*D$2QRxr4Aq?sn{rMet2bSB`pwX~U#^p=&CL-eX5{@F5bdIy08R zcq2`L(6TNBYWO1GjBp7Pd9|L8S1+h=>7%dldk~%|BJ#vjHZ_JJ`F8L#pqGVY{Lz(2 zYgJ;p%$7!HXtO4Q#07WhIFzK)+qhhKum^NcbVhvZlHHa9*%a>5dW!Yn%joRZ;JzU? zl}zspKeVDxPrM;&xR4Mxqm!kQr0dY3Y8uh<2nYjbK{}^v4E4LwVc6l0t<1 zFt*5m5$XW!Anj!K8e3sLePn9kRiO-BPM)<4XZiGMBZF!6`Epb9>uKz%9tdph2^o4b z9?!Pk5FabS@<)?~TChZ%NRyuCQ$(JF%x2cu=H0NpQ6i)WwUOLC;Mr0JDO6p5Gwu>i z{h7&%$tzeAJ4@tcP+J4I8U5;#K1K2JWklaX^4M@TB7M2^g!=`PZ^BtKU1H-z20}(_ zKMb`P1zSvh%j`O~rLQDkXm7VDHLuUzw@(Jw1N4s;)1t^6pS4ymP1;II=|=C1ZqV)Ehn25IKw?#J`|9s}!qNA7IsC1Kp{7Bcoekjd<_mUB#X2b$N&tJS{I!tS z;OhjX;BVPt{7uQ-8GOf&KM?+691Kno(u1Inab;HsQNG7qZ%S$q;HC?$U~`4nbjvru z+Ar@NkaF6MpaYwbmG^%v;-`p%4;haJ3(2{3mnq-h{UF#v+EzDc7VmX!aNl_z6W7pnn0rN&gQQKY?j+`F71rz z^BZpNkd4bd?KSX^_gRfA;jmGL9?NZ-%cavhBhl}34;AL~QKrdG37cfHoR+yceREXB zy}(WqRAvVbygi;CRHu3+$PCXlx-Q(ea%i;huvQkk$_l@S63SsGZ(Y6(@MLXZPxp1$R z^HqAN9c$1S_uzJVXU%)&Mx;mMMui+)$}fctTMKXGS%2zy_7AC>Mz`OYugrUf-rD;1 za0NMq#ZT|q%BeWQ@AvJi0OPz&Gh2~mJnF~D2U8jvwsP9B8|vc#U+abjDU z`#Afk7QPXDfe=unpB$Y{1O77`X?~jocuXIXXRRBwW~5+gywN-YYU9ajLoviCA5@f& z(oJKYJ?Vd~!pG0l9&bA2YJVj&6bI+*FO|`>BjHmvKn7HZsqN!{uwRLj5(wsW4vGy z@q`$`SS9oZsP#)9snZf&AZ@vAT+0*5D3@6&A?%}qjVrvXMVr{>1Wlg4*E6@@kMS4D z+@kk5MXk;h=xr4#0^jl#1nz+!x|)qX`;O8QYD5o4OKb^Kz@kD0OL>}nt$C$nFdkjI1`s zf8KT8AKjUCh(uWww=Ey7*XXvL&40vvCfHw8g`@7~avq|yzXOQQP8$0Vo%EiWaRFBa^Pk;ft*PRI}o0zx@Y}0Cu8Lnw;R3;DzCFW?m7;PI2 zacXPTwqKmQpKYgK2gT-Z{n|?)1Fl1E9)|cHILrCJRb3{fjL^Gv!8?K4w~?zr3aWCm z!Kn-|GxF=V%350Nim2xpAm>d4ohZbvzsdXy8BDy9JIP+m^X??ZG4P(8(1w{94c zQS@tqHuTzfEF%6|OyL}0?dS1dg?1c4N*M3wwWYWf>=wq-371*J*}s!>T*GVwT-6O# zPB_<5W{lMm80YdH0V;x06_5UrwDKmSH7%(}M*vJi2H6KoO;VfXS>rItZ>txGIp%Y% zzqc~=t!3UH+F$$g1D(aUzfA$Uwm4qnIKOw04Z7}&ScG@6S{d@}&)%iwSKWk%a?bEl zbJl)uoh6Fhj*12Mt3**v)_ZU5k+?nDxZ5%cT#-4d0_;`(n=LE#$AylZ`;loM;X*z7 zcXbcy3CC~M)|$aLR%`s_DRYBWFgjr;d>#TOgzc!a#)xfKpm{Egoz~)5yR&*Bx^-i) zvRKir+h{EN(Lzh`%dywA_%onixA;a8Tfb=GBMiRf1Q`6+DA1vqG6A zjeeBet-v`U!HX0AjF7mi@%h%C%>=*QT^nnYafBj$QRwVf7H!&wTFK7nwwv!-|C5ZF zg;aq2Y8NU)5fiUsGfV4u4d*6Mf}rZC?tb|hqQ~_{gYj&5?14szfjON9!N~0+o(mX11%z zP#_hfe9&NXDD`N5qYlrIvA3Ors9itle(zhKi}>#j0l*|xPbpt!=M zdX9LB6~0v?-Lh5DiPE?ECa@-tDX@O-Z~lt3j94=%QpJ=H`KHuVeSO6?3AMW({|KOT z-i&2V_-2V@06R)30|RJSH;T8ch`>-}O!-ax=W=Id6lZ`RA8ZW=39HOEk9%YkV6&R? zH(5@mp_{qPu^~(FL0X}QZTJcS81+qGTNC<{ShF4Mj%X&zJf#~ylQT+>#0B)J;cST1 z0@;x_o%e2$7L>6V!0h0>V`B`Eb&OfEWoQ5_S%JOoz z8CyE9_tGilhg6%2$60jQjH|wuwpJ(fe}M5`pa{SmW!um2;dMqE0SZ(n9&lHsYv^b{ zhUTFs-)6B$eNh%Q0_@EW8Frr?#nf>Op}y++&DPdd<`;fi@|jT>70LXQ|GtT_@o6n? z9v-OYv7<+GWt}|ACp>8)872rzCONe;e(CDi%Dcl|Le!8|8Yzn56~`i{!sdN#o=2Y_ z++?WsJxn_Q!&KgJn!QkF{Jg2-rV2Yq3!!RLRB6u-t!TMk=XU^K(MNCe`e->fH4@n= zspj9y8k_1;hYqpXN~R~V`5keuhW+PH<|8PiTf1NZ#EQNYleH9IrVe3Gb0bX_?z38X zW5eHYzM*p{bV8O@FdH7qAmL1wKZ=ct;4O=~ZHcHu-BjRd`F;E3Fgk=xVAA~@&j;`V zf=FH?@4+BG_LMA?ay&o#H>U>bu`D8WDgZa@scD@|2N(~|@x5@_K8qMydVqg(-#1=S zHpXm|%oN!mC6C}%#kmflB;!xWL!5M`bw%012ZltO}+)xig0Bqhj-Fx%Y?&S!pCxPbhlR` z!*_$S+mWpgK~n*N&WdoAmLj}H)C~2lr}w@@HBwbVqkpohYVe@c-H>5qMt`%;b61?L z|Go-`-L(;z-6ddLHxmK&-CIptv*F*fM@t;n&l!sT0^jE?%GFVhTfs6shvp0GByrRM ziq6xf=CaJkw35UlnXDh!+%G3N<=;Jydk@yzLZMLR*984?+xR1EqjjEG%-r%H`B6yq z&2RiBEmV|@^TMGP8ohD38&ScFLEQ<3S*8f(KMu9+N9sIh2TfP9{j0qdP^LF@10w13 zxeUk!=VcWqI!;zEC7QQCgN2y^=S2{ZzR%f`K4%BgOto)}-tM zbe>s4UIKG$ma?%D5;1#0ekRbpnY}@`a`03`&dnFm$I+zHl@GV(ONxybQ2KPUsUVJF zJ94DEvR9@BYPQ87S1H?Bqk)S$&i^?`&f2~X7j*iTwkLqcNIk*?(QsXTub(My;tjULw$u|S zWPi?N%$6_Wpi8p{76kJzWQ6+--WG}#?73yDtRm~FcGVPZoMlUVGnSF$N0V+8Z%S`D z67bA}$A2#}m@$)&e!mI`21gTt*EqotFexlqFn>I-CJg`!QaMVnom^PR!R&<+zRHZ3 z77t@)?l=1lK8EVoGfu<^5Y;nOwD>Qi#{DRRy{6+Q^6LrKG00?%7gZXxGU!Au@8TMI z0Vr_Y+;{e9bKK#X5_1`+!AZ#cx2ew3vNGd1kxZxFy{?NB*JsDkS$6C5Ww4ETHR^ob zR=K{aQo%maXwsX9yH?m6`>L1erpZ&8_dM-* z%N!vyUEhQa^Bt4DdCPP2`g_D?!fJCJxI&$V2OK*D_@0h&t71^_7;)Hh%Tk`8Dfi%Z zVt1C>xO2-jKudmS6cb=>@H{}`KB;j|?P%a(v-nvV9X?T8BftWOJ?|C_vF3-j0GUnf_oB-@czE$sokj-F{btt0=nK7C z5uVgs&S~#8yU7bc*&yK^{5$AvJ&h~z=h2l1PE~(a7jD-gp!ae89ds;I1f&lkS zJ8`D{qYiVp_t~r{-ZCHrRhGNY-xQmf0iE+}<}9uSW{4t>(1;%J(lgaENKe*Q==EmU zZVCz)Kv?YYXq3Nl8*qMj(N3y~r(}@uPZT5Z@Q$f09E#lR64C5iyfa&AkF?k2wo4Gr$IC zwb)%8CBx-53+ul)im-HzeLU>g-c3#$qtGt1r_kF6@*e=4z{+Rhw2(T${D42N%R6v{ zD$6U+h9L9xy>oEGWqgV&od=N(5cdEa@>-!L_~9$xqU`DRY*n9Z+@q8dZp#nex5#>(<{+k_XsH(BNjMSk`z##mnim_ID zD1e)6A3cVXAL-7KxvMCBme%3%+cA| zIP;fhC{5amtUKB;lM0aSuxP^QfP$EPgSYF7#dWUi1Cw^fH@%w3_=Uhe$4b%27(`}~4?^idqZ zs;G#oR9gEc)BI}bgGEZ60m{S8)2m~uz!z#{4k6zLyc2>r+VRumWj)F7uWqIK3~n7k zyYx&@J9ksJ+P38(sd)bKz@W)B{eodXJqHMUZgKsj4tMgwIN^kw2%k7X^C z+J8CUrUyRvKcu+SFz6pYd0|(^^&UWd3Ft7N!Uxu0jqIJ8%mm+T|?P`kmL#+fXz{#d36JQMUSNYB2`VENHqTUJ`+)ySdvo4zZ z4X2-zPxEKhJ5GxuF7=LazZTEJGpQ-&-nt-W$e8@ET*~>J!2rNYK#SJnlm#dLzVi(y zQkkp5_2+3G0$zFVq)-xM>!$wd`fVF)Adx>ad{{o{ujD#+at|VX|3w8cps!g1)U^AH zGIP~f{cPqRM$f%aN5ha>9P~jO1qGN`p~-jKC)veCG~uU z3Oy&_EucMIuAA0$g?7`IiG-oGI4`{}ppwBZ^~0E8*SJ;J=IOB4yGhLR{>hW80lZv2 z59=#!A|fK7t8RPm$B=A|BQbWmsvj;@dowg9=W^wTPp{A z1}1nv&}hXm;^};lt%sbs5AfJpM$diUGJs3u>ayZ*Ds+vK<0?Tp%SyEu07e$U^wJV7 zUHx>8#~YOA|EJqvh$B_ObSDg8X(_0IGsdg4oEb$_g$&r!z-3o)6E$fUt!u}oVD{_j zlBU%yuCQ*DWrFYS4*kD7lR0e)iX}>gfJ6t>5MtRG%W;b*gGY|X$k}EA6SkyXI6!C^ z(BtPj(--}M-H;|w{bj+JUh`2LuWEL{*6%dxJ}$2V+}uFln2dg;wc@Bxr!@Ee{e~QG z319}J{=@Ey^iHS^QF&Kc|f}bZ=1733E#K5!K5G2T-?tB_LTpt!_!AIw>969s3F3 zoEwhF`V0kYKp+uM=HbBOLpI;x;))Ym`pzBr`60VD=U=vhtA$iMjV2#m|KG{q%I9Q< zlJ-|Q2!-fI{j&Lkx&dtc1NN7*_a!9UFQ~-`9ZSW&A*gErRY3pL&dDP7vv)en{;25! zps?D%G!xYdYdVA9J;t`%_`mIJwf!N$KmRp6_w&U6cVzGQ2?0Qe!qGYo0ps)YAerww zz~*QtTf*yrq7$cDqw+4rN8#U>#hKnAYCw?%-K-Xde->)X-FH6cZLx4qC zc66_xXY5ChIhk#R>->V7>UIc{7;V3pIX=(^5uq-F%; zUs)t`c_$W#G_;yb+)j2DN{&*X=XXJiLW`9CsiO&7E#9>kfsyk626&81SewDhc)4xg z`z&Ly4*ydmbL_diiaAq7zmP^$G=({4KU(|cc+pKB6KI8aRUnPYpH%cY4Rk-dl+k>S zbUcW!y+02}9vx04jfru%~;-iA!1P?$)?p~kE-Aw|}`_odo2g{LFR zI+{^pftjsBitwjKen1;Y^{0AiUsU?)#A?k-VO~w+PRIVvC!EKBVKblCZSp0p#wvo2 z0rgTFPr5Uukc7hlK{Ljc8y|;8F`*?2$*V8A)j|GPe|r36ScfO(B}2106}16usQhq(XJiEq1| z+=KsfKRxr`Jl9WYtM1}behHKu-jv#GwO-6)40%B2Z~PJdBKh zd5qMh+a-i$^sELcu!=tnUC@G-4qVY(y3`AF6fg^X=f#@QCtm6*>1!$YD0uV;auJZ! zvg<7@wY=Lxdbtr_Z<$&vy4Z^=Agn*ja+?MxEeHj}s@)poiPKWPV(tE7!)a?O=woP5 z!I1;ckX?$X4npN9D{;;VDg1qGemTC47Tx%rTXiYdF0ixT&%ek{@?vw;=RbIb%WD*B zjWSBd0lxvMIx`OjI&q#%ln$ulHBBt*@V=(DWlqhWZbI_=P!nr8O^ezfjIHz%`MT~T z7Hb!$4f8BPg1gECpM0SinWR=?>Np)3X|Zwm`LcY0vZ0fE^TS)+MEwN+nf8W#NYIb# zIsE;4_NU2%Zpt*xeLQz{=^JcN{ElIziwOlSLVA79>BQ{g{uCjH7~%;BXA7Y9fr)l0 z+}d(7`h4FD-|7PVWs91jlx27O7H5`oN`CG^r$?oi{(Qx9){je;o|h2Q6makQgt)(4 z$0t%{v0+0~R7EXn+V$@zxPli>#$4>SAE+~P1|!c9ob~Qk<83VKRb{U{OFD7McwM=; z+_262xWl>4G681q>9-l}3yhQHWb5zk3bnQy_j!DrzRD_bC4lc4viL^c_b&v5QUzJW zD@1JvaSf@fthiE;^d8MWB+s82kjvSEOuh1~C>*2NlzpXZv`0m13pqsVUygl`J89?N zT+aVUwNUfaRlS0AC)b-m-?h9pNvgDy0qY$^zd<%LD%v^+jwk|=gAHBTkP~vhw zL4)kxT$q{T?rn$Y)HB<}M;MLiMq(DaI#QC9ed;ffFVCas-kEW32DehFn=QGbd;;RO zi8+h!9IUE(9Lw83X^@bd3g$y71B#fIgVb@+C3p9A)_tBQYqhnnF79ovu~5H%LJ9$` z^yY2rtXKT9W&gC>bL@AuvhTY-ggI_QdD05^{|t@cZpjf z&?xTfZYe5ms#LPme3n9hJs7*QCs{WY=tWYaO*8-+dZd8l`v+|-OI|w!o7UK;ZRrka zkcEKW1#=zIFXBl_Ws>5p!J83%i9xjM4l$Jy@j2 zK8mp}mtL5gDny$`PWn5vSfbR?cY2>>v9(uq;z8o-ViGb+RZ#~DCCXEkY_!LR(d^SS zQ$&DH1#jeel4|>o!M^Ys$64Jg?+Nh0@S!WR?rq z5|En9Q1;ue7bu&S;{X}?xh?X3ZVBrgafPx`P*$YGjPuY72>qc~EM{Hu=ib!j6!QZ6I(t-NUU@%eiI=1?cp=O>z@)mQ_#WC< zdybqCvG#@>z@4!r3$rh&@*UVW?&4+iB>|rQr#)Q1;u@uAp5i;4%nUrDo0OQWszSjj z)B?`R>y3z%#gKPQ^#G_>nMew(CPTn+nntSKQxpSh$o&;U!*{t3?AvmC!4|;Sa3@57>mnEI>f`%F78WR+? zSdz4$jj99Z*fPcf?4a~FH8;C!pP9|B#hny#>U;{%DvGN*T`c8yg|g)QA#!p&(z=I40NBb5(;)wVg7u@mB`ps zwW0jVL-~`(44>%o7i$||bE2$Zb01>}N8eZ`&B-eieoL=(DrS+)(rH^PP+)&azIf}a ztmMzf$1gt%;;+FCHi^gUtJit!-&Prw%i<>mxl|T0&IT|;dwYT{bC?8ij>T0FqOR{M zsVQx=w1XrbDXL7J?PsJ+S6x}88q^GyAn{Ph>cnBIq$34?W|W1zX?Jyw*e$+ZzF6G5 zl2T0RJsoy0v2l1x{=Je|uVL=88?<|UkNC{%-7WCxbL}0iEfMQtxwls9A%;KPAv&w6 zE5nA0$DPVDto6KDA}p(OyF2~EzFlje5G#f&-sesie*@cxl>yIH#1+1q{j^ug-I{n=wS zKxVJ3%s6#!Ki*VisNWW4sAt-u$h*=nH5&$x#_mcW3pdU`o>~FK>X&a2lbY>+*>fZN zx3}T4KUC)~ol7nInou2XX(B{FLPMO*Vwnoo?0B# z)zOq69e&)ul));lcRcCx`w21YDc>@kn67LFk8X8zF}yFVpH-fl5F|!f*&NdPB@^D| z5IR~9k@NB^PZSc!xuf%Go{d?ydWeNf6rqesER50Vq(r%EtbnkWmGeWaVv{T}Ebp2G zYudCVLA@Nym#(~bdY#`*!&N!2dhU9hdNCrD^2gxv=ELF=vz2I(+!48`dHf%T%AnFF*hy?WRqHpe>He6S!rG{y)nH?+N)R9kWJ^V zhb*4nTjf-d0QCt_E~rzqmfK67scsKM9AHJn$j{6b zI4Wk7reVWdDA$3^)PW`;CH9LO6Gjbt=f9#A$-6ADgV7jU4E(}m$G-Zv1Uhtl~Em;J6$*7N+P z9{lDbb9~Jes}0FK-rduG+Dnq(KJtQEm{xui9+g{!OInu5ON%L;u>#Fl{27X-;+=r#f{2Hixwi@=J3fDXPkmZHZVXqJ(y0v~|vV?U1DOBhZrH6aYc9*!=3c3IChNJB?tMlZlxHO4O0vBE0Rq;g6vXr-+pfLr(_tqT6{La1g&GJ9%*k( zG|F9XaxAvd18<&V&InWzSNG)1N95*?5?_Rnk%2{7W=Ec z%a@?%iU^IYCZpL9aSOwgi(40OPS>DC@Q8))iqPXb`i zx@+T|fB&^^>mRIIRh{W_o|Omt8yT^|n0KX=m~@#M&(F18W);q(t1^tgzL#(K&&@2` zfbuK1qdqz$A_l^+*v+T!=MTPaO+~CG{Z`i2=NhUY=cycj5ObqJbhiG5)p|+(PC~Iw znb5JbQw2qTH?3I3wApicL~%kLdNQYCtdPt21y_x1Sv#~;1@IFT0}W#F2}fNq_Y ze#vv-ZLya)pt!#e@6KQU?QZ!A2mrps$&~#mZYux@1E3&$ulo&9tC!M|htU_Ce?6)) zU?OuA`lASJnyek2fiIMk@m3+es{?yf!ib2PMtS+_T;W=Im-SmT(ssh=x)-k=_Q}II zCTs3ffcv;IpFr7L+426h<(MVq8?D&xhT6Pcq;ka&n?$+0SaV#wPLrddeVMAp^saX^ z6qEa$Y%wS&MVvjzFZZP%RK0oM_^&O#cc^G-m4JzxK||{e-?biGwi8p3d8}wQNo*#% zj8V-_m?Wsor7#!k51-);%ry>49No^7#JP`d$8KQBNrA*6^fGS{wftvrM$Y+_`)$Nr zh;CTeM#g}4Lho;ay7swB>c0?%2c$Y)Uod%9jT_sJIGwHiR$X3+T~y9H{5KUCj4ev! z5h1GBgqEeDA8b@EMi#J+i~o-%C09}#4J!97lxd@lYx?6b+q zzLn6PiPnx}JuUs%K_k!p}9VKnSu`#WK}t2YGHE z3c9249=7J_C|&z`1!$ZM#B8Ub%9;!SAZOpVlv7Jr;`|v2-!^waTqn60 z-;?NRjph5YJxu<1)BR~Bzj@3zC6!0dTb*G6CXt~Jd*=GS`~UpRk!=8mDv;7Gn4 zA_D&=1m!Z@2(VQ@eiM|M&FSr(+g?y1`oe~3O;#aZ*0y!lx=*`KGjNJNHk=vWWjgjU zb9xa!rzq6=Vo37KH7)J(e@nyvPny0c0-3*W!dm%b_jnrr?*+dZS{TqXpY!8C;&Tto z{~KM1ufCi;_$)N;Q-8|o+M?#N3z{6nScxKND({FXD@!~ZR)Y-LX#}WIm1m&SE5@aj zAmoHqusQA2*xH#`nSzwK-&;vXDPUTdrl1l-ALArma|JJ28)$>uwx_KIZVB#Gr z>AZR>q-~nVH35~h%FT15Jm;0(Lp-Yjme zni?;DQL&M=k8_V7?A;Cq-RoA_pL<*snkTyO`z3=w;BB_;^fBo5-R{*^s4n}~%JIcN zmd+|-1XfPg-%gm`<6}EFuXRlxYL`11WEa=ts3+j}hs2oYA2-A+&ouy=8Q%GZh04y~ z7HArv^{jOVLw%p+u0>DX^Mc4yWnL4pOJG)dKM`S5A)ymM{@OnB%n$ymQ2}X;DRVpif zlC=j455!`6i!n6$#VqZ3P%u@_r6iwkKtVYW+%_8;(rzTX{&(S$F0#C${UO7^ypVRG zj!Jm7Xi2G&2_S8lo0iI-jEaF@NKbu-Y&6o{ zkb-oj^IKxYTkbE1T}>1V&qS_ot->T#t?B~tJx$1OehAB&YtR$M0dprjbkfAFO_=(M zSrwv>YrfGM+Sv+tw9nz`>3jHW6y4ALd3)7roVu(edy}cIa42XzYI-e=9zaMF*IlcR zikKHjNEt+KuYVhEPFvuW*2{rzdhUttx<@!iC8KE5M~hN((<#sD7k;oHJoIF+k`U*<%tm;ubj#Om0M!Ku z!T}M5I4P$LfQdf&mBg2D4sL&8*Pd*E-Mnl z8$L2u?h#XRei+DX!vd~=3=LOyLb9)ub`ytEx(E0$r9fXm)T=<{V!4{MaWl0|9yxuu zxX^+eAZ8wsZF<`r$k}<7bbY=~4_Q~%BKerjTD?kix4(LxUQWJmFa@AxS?tTA6y?D- zlKdgP_8?@FmeNkoTPS(Bfxy-sh;v)8hd-Fp6T8~2Ehce7G5bw1U->fWO|S&z87(7e zFymj+mV>-*t}dsES>6xA(i)3DZzddA+Mh}G1l`>?^pl1zClm)5n|sCqB1=`+ccXJ? zaR@@*ukwN>(EDg)Sp()v>+%zoSJdeU=!Sa09?`flcd6Rm@~aA+HggVsSqVO>ed_9T zyn5%Ve<$sI`Z?v$kQ@p(o<#awe%0`0PyX#5=NK}PL2J{1ta?RZQnkcn zuWU5b25b+zIp--3$%|t^aoM%U%-!5V%Vj$Ut5)q8*P-fce0)1$dL@kxBk-lyMd9ZK zkmpMZ>pW-5txkdKxegS}0Uz!uQ(-yN3tUEP=L+S@B2SUOmg{^~dW# z$Ria9ceqwe3GOZi5GrD6dU}x2Io|voi8BNH5vrt>CYDj00pHY&_e|GO$*-#9&EGqd z(?$oYVHx7w#BhD$$f_xvFCZ~@k;wHtcFe4k>DmwY47gt&Hnfny?Z2&czQ53^_yR~X z10Y)C(fhTZPCQ{ORI39aDKleBVI{G=NGQV6>Qt?p>23a^LanR!#XEg&#D4p10VUKm zS&%4IQd?{F)NBcj`Du46zZfuk5XTt~WA6hUvcu#|9Zb%Y4JG<$$m12CA&@T0@xc2# z?$Ce12?g<{Wbvxxd=(64G%>dj?ylTt84Buiieo}|*J=#`;x;!A`vCJBhqH@Uug<5J z#|e1N1}rV6VdDhQn|%0+b1-^9#CgD4xZQeWUgXNaQWc%9B#T{eUAA#bIjTFId9^d2 zF`2Mlaq?FoaxaiHiFLPxGg4MUqwS7u_CylvXQ1t*Bxh*X8 z@Sam`OE*8fJU_%nokdTx399_-Gs>ET~Z0pkF3FBXac(m$m; zQIF2gn+S8ugjoC2o#L$9NiLy4>rcsCr^q1roagT}KdX{P-V2+>i0&1Z5?vYy1Zv8c zN=i8%4!0XS4vE}3=G1Q#j?I$R(4y$4PfzyePJC6FWTn>G{jo`R+e}PL6Dgx`3tSTw)cV zuC(|$<9Yt%E8f%^*FOl@MHJqf3dq-gvUW)sf4a9HhMt}{d$85|^$ez0>w^J%`f$-* zF(dQrBfO_0Zve`-8s8wq?0Rk#g0HVxW{wX+nHZmAHrk%-p~<67b<&{i`sBBkfD>q3 zw7L8XA~)9c{L^E{DU2xk#ydTIl|Z)`az&mR&o+vehBKeP@ci8E`rpBFvtw2OtX9Js z@%eH`8_ca$l&od4r-msz+WSgkOm?E}kJSLSTuu?GG8ThhB90QyXzimU>duq2TggNU zt&L271B3bkBm(InY#if^uJ3A7phCB48hAbLS_`xb2kqCSWrrZ$5C%9BNuSPe>gJ*! zd^;lIUA7&`+EL*pZi+c~XM%OYN(0P{UgHc|ru4mZ8Q|`!b0QtP6P4(SH(C^BG{-oo z%k*Z2P#hhHKp7XP{~~v{s3h5PN(`o|eXblNSpZJO_SGMdo~6QH3&32 zE#4XApBRN`He9{;s%Zi|El@Gb$sI%a`FeR9byOmap-lP|ey}X_!*Ef)L@j5v|7iAd zouG!0CYi%;ZN|Ec56Y&#k}uB-X;iy?bdq@O0Fd;-TI+p|x0|aX6j=?l`I%i)0^9EG>iVdZq+rkL+ZyQuUfVc*%E-4q2p$_Ic%WQHE&Weg<+y46|;nZy_;nxEI z>(mea^KZ2#hrl2HIleypUqUH_Rm1>JXJ%x&pv&w(-@*0u#de+O9#Db(_3uK_b$(7I z$KrlJg2wqUP$C)Rc?1#b z&+}}>?V-BzO4)+?uP#>=sUeX!+_9P}L7CiC+Ge;+c)JQUGzO;n+vR1XObtm(b(wZD zyThz6n}d*yh;g)jU(-L8wqgxG0cXvq;J!Vrw1{C{xXI2Gj+>3z3C6tKQQ&q-oFU-? zc`}hN7W(IQm6NEfF90oD^NEJtdQ??_0E1;w6rz%!RvdKvnT%serg)fJVO0|W=mL1@ z8Jm?Pko;AFk(j3|y{3#+xD9@-_>n3HoO_QHGTowfx`85JONE9UTvH+gESi%x75Ec; zJF0p4vEud>XX>JCO;(p3&2ssgIGj456sWvOap}hK^=H4^oH|)ScB#l?gob_Fpko2W z7;b~}v9HP`!$KOspiLCB;X5DJi2f{;y`rK`^*VL_y4Z|9Vt7ShO%OzlM!Pv-6!W(r zjI!FY|74-cib}hVeQsQy@yw5g(7{c0*Jr8;6rgbBd?23ud_V2d`ta%lbB+3&tHK?) z4v3rxsQLl*7(fAOF6hvSoYNp+APM>+ADH_&LDOYqVr1$tteMGS%dqUyONO;-L&@Kq zTW-m_6sA?2@xT$xII;|>fc7LWP|ktz?>8&&a&FP36T|6AYib;I}987L35@xH-O?PQx(rc@om?t?jPVve{An8W@mXeIaSjy%NkpW1o!+_jo zNG%L8$QlSm718i3K3Qd!O+AdI#lCVo_8FmpG|UGWGBu6S3hL8z%~Idr>p#(Qkif)j9reazZD@a1RSt>gZ94pT2^&|`mP=%a!#wpE^A4?|7! z$Z&Ca`(=zZ$unT<_!@RQco%s?fs5`AT<=%DANbsBHhuwOcDV0>XNW99 z12w1=0i?8zB_AFHg2m^41@Es(06Q)&x$h98#AVGL$Kn#&a6&CN_>{|>#1C+9FHbeFB;hjp- zE_)W<+Kv<3eC+))wDex5BWkalldWT4y4I=Bux|9_8HJ(nxp{_M-LBN@!W_5Q{T5)=Oo5Rc+!i(W^-6$d&E2tu*NJO*3w+w*T^PuJoj zLOoM4%L96lXI|}f(vIC$=+~T}Wj|Hht^t&c$Gte4$Eksfy#0|M>?%NfFfpb77gB^S z@Vy-V_PvT~L$x1@0o~||;(V&0sZOLL#T-awlu0l-P&BJoAt;s>2Q`={#_z-j3@aXC z@9!cNZDMY(Wp{o%=5p#DN>?l7k<=E^(9RFYhv?=4Umym73^KCJiPf*X-D-NeY;UH5BKY z0zvvoR>6X>h=`s2)y)8@Dj^AQ?d)Ga$`k;7$RA_M>om*Gw_k2{)~kVyN-c-qo(A00 zBGZT$naHM&vvDiDl?t%Ha7qafv3R?vYJh~~VgB$COQWstmB!N)qH3=L|Nb^==#uy^ z0BK8Lwl|aKoAV>28AwAw(oFhikHzeC7-Pq_hKv6`}E| z(z$)}ulujn1*GZXT23+V9OP;GJ9Ipx4}|7BhF|Dj(p49d!9?%iy3Z?I_`FWC=Jt6D zs8WT7EuRhfbcGmD)a3ySBn0DYO-fUGcUyMoL+{ZnBxyx(zIoW+DM!B6KsGkkiIG_r zfzSW_I6vgoLW$iUfZle#RyMHJ%&DDAvA0o47D!npyS<@2NV(xReLMN}8V0Auwi137 z5D-jkW~>M60fmc)SQWR*dWq&=?;YFrGUFU5_mksS+bdx?rT0@`w+xOe=Uu%Bq#8M>o`n>-|uXr5orpZlDK&oeZ4m1 zFIN(UA$hE;Y!~@Hz0YY@QF~VE zGrX9(>1^6Vewh!!DtEE&DC2g>49H|?3mt5JYD(x(6kR{S^-j!PX}te_OMW}W+3^O@ zF=^e@5HLyWmf=*SB~)ag*0BW zuC;zoS(k6Bz`x)>YEoFL^)R8al>8YDpvvO1Wu)#q$-~$^MVXe(*>p4qCfWPt{Nw9e z2ma*a?^XLt7Nu4K>l2l~GHi5b`nIiQQQ>jn0bJp$zOXYAGRC}fOP$2FPbCcsQpVQi zC?lQN-=FyZP0*##T(JEcuRD($LagpCK(G4 z4BTfh+9s%L^+XV;D6hQY3dW;-Aq-tZd!e#6^dm&nI86n_{bJcQzuChWn z)FtLB_6R+%1p4svJUOySx^j0%cV1l}>3^9O%)}{mtb4t~m`96osSSGrDf{ zE?8YCAw-T$qpt%V%u-_NJcChO$=bV+z`h5&6-+L-WW?-*338d%8tZlSKg3BiC=|FD zNT?t7y|WQ#*q8#j6Q{!;GaXG5%5p;RB3y<{gCp)+Pm{2x3YQ1TAA?^{L9g>GhaGi) z_K>o?v8r2!b+PL)$98%^A|zRE)3RP|WjGA>YhabffL>=0c3EB`+Xjo~oF|YRR)bgT zWo@mtBVy(**jPu0ExHZ(znuI0bP$s}vsL%P4Qo8meJy6hxR$I?HRc=Cw-U6$S6CSx zW+{s=|K##!!7j-8I)9Xe`yp+W!Zr7TMe=mUhasnAgfT4U}?)<)-;M6$r15aSi?Lgg$iq8%X zW}7L0_&x_AY?vkf0Dax4OyymY?N4sEAG{ro}w zF&z3ER*>gz_X>w2_)2-Ew>-!9KKfVqp`8xrDbII63CRRTo&B)mNBdrQ@wl{8#`%M7 zPIPWk>5(@lf`i)AG6c4m5b@|fZVh%HVH{vHN=^)Wsk0-c$5klx)xK*67cZNn9?3pD zfDXJf{$q!1fRT(xK$8QUIj17)``O-1O6{FQc4kDXw0`4Y`GBnJ>vYprPUXM@w=iD&sA8Agwjr_ghF5e#<-uk#TChfH2!Orahg9ncj7LHw>_!WMay_kyOdjD`+ z*ysHR7yC~X6k^*90Yi8q^vU1eyxEn%#n6p*|K}I$)MG>n>}TIEXH-`DIuqQox}Wch ziIKLV=0+eBs^t!Awd+8~z4r>pgmwLpGvk@}0&kD#i5iVLn5O1-hbcb?8D|b>Oj9T+ z&ey=Vl)w(pO`eo9P+mZOtEZa}vSBOdv!^pwT{sz}k%coLJYg3NJ}R6xfH^df8Hx30gK+RdZSDn00$-u1k%Vn5=2o>*u9!^oDA4hw4pC3Dn9)4BSH zxGcly>Y#GT=}!e{-Ldg;dV88$@jHX)sm~Y(ym|M%X!`vD250t0*m}^w`|dKPC%j9W z)4rNZ4$6qEBvFqLy}B|hv28lAN%bS-pLejWe{v;{?w;Od$7dZ;w>o{n^ejoLop;W* ze-&awvAf!bdOM$&W)yaVA3}NgJiU$??t#9rp-C5<_pQS%>W2iyCiGg^nb<6H2@)F3yd7BD zLBr3eR}tMN7Sc+?S;g?9VyjAbefr8(!n2i`Cpc5+)b{YQ+6~f_CpOzWER*sVVM(Pe zmEAcNcqao-C8(xzLd_eCMcn!6RGU=Z)aqy%XaQPZvo*ccUPhU&#RHZZxQTh9CMre5 zQa;K{CzCw|--($0W9!_BmXR=$ps$0o#H~wQRz|&E;6nswC#l)9ze+-s6FSP7)Rr5w z;m4h#$zy@cj=YT3TNI!3DTaz%IdZ4+|IJiF|6Nfv)Y7_|yy6haOmPNNztRe&n zqWrF-(mFC5JK(>)Jtdy6uMYzoO{#4xMzkGQT5+ZN+9Yfy_m}#~Eo!V_x1xNGmIz(Q z{m=n%BF;6OQqMoV7BQp4L`JUYp<18u`E~R+vnZVkt&I|27WzkBaFwu22kHs$C@J!3 zH5*eM$kZW?R^D1l&hNOqcZ3x^Etg?QJr!1IatR;BLOB{Is$wnzmx4ALwtek@QJ(Lm zlAYYvU6#AIByz)%L1$ABNw6&4*?w#{qyuN^e98cbFlYg z0uP0FS7QIYn_cjh&)2v|`fCmdc41LoL_QP9;D~%|n#KOyDUuptL9g|38l<#$7NYXj z$lzTK?vpb@m$oR@o=5wP|jTwrSAw9qSgoQTOX6WsXMQ8`m+0ltj_EzLIMKp8sih0 zCf4Bzm{W(2Xgo&H>Z%>W{5%A=B)YP*$~DDkDxG)(16$iZyYGPPw5PT zn0U`o>MFIOP9)E*p9lF6(#ctoJXu?~wx@CjM28tEdu6@8OsLgC!i@S|Y@>MG*f!XC zcDH&H7+kJs#qZKw-uDIkZHun?u}%U8^bJkx-F{$(&*%QJ)Llt*`%-zeVgL7Y4=+^s95W;?bRUjk*5-726LL6PvU8M_=m~gV@&^QWpb6a5mMYo*yyphD%<>?M z?t;mwUhw0fF}!~}3>tQr+OvIp8LQY**R&*8`g8}IYghQWssO&4nwgi(<_A|FclP!K zR_97-?v-@+vK|EU1W^r>qAyRG@mWC%&E0J)PkG!(3^Rt$tqaDJ{afRjU)2}>C>2c2 zKNn_#=+E~)*l7o`7{tL;Xm}+TeLsE?$uG|?xfJAE9L$`QZOw+M%EoCbNmBGD9%;il@&N&s)`9TV#%IQ@ z<9TV9U}od~Rv8(cgxS!e(!DXv{!HGIfh`HIFFWZ$6q+ZIKkGH!#kp~_My~GL+fc=@ zjS~=c2&ctsWI>p-#U+Nb5;87h2J7!^-~q9+CNgxIW?6E zzTC$=vg_Ej=GJEetYIqf=&w8>NkcvV<(VJ?()VhV1 zoNCq?GY@U~U%toompE;=M*|yLhHA;`0zmTcv!z4rGZKwfA zzpLQnmX|b~W-FS;=R_mJD8P#NZduzfHOs@elvRzKdo}){P0#6u&+8k3W1~YP9DXf! zl*|t$VDY%6^cv&Z?~PZ(OuB6&*y6O+kCuwO*^dWTBM9d2ELEgT^l11~nkq#dWqcat zELm@O%iMG}v1*}?`ccUyi&ry{LYvddz5C^3>G6u_)xofhinox}F&d|=@>Pq!(rg<& z(2NJqw~^@`uCTR=Yq|UCnd;)g$ClG~gjiD$f^K4F_);^%q|S_EL4^BO2Ueg-qs@~+ z>S#c{lMq z4Q}JDNw`Op;vVqa{>zenWDl<9fnr*V{m6sc>)gzQXD7Hut}JqFxsj2arN8K~BNxwa z!m=9m@u#bAsn`1cRTw%V8SpZa;6n3LG;B62fE++>mA9?U3+at}yHF|1c{Z@l$B0kh z>p6#4(I*KoL$;5?TmOwcy2hoVf~*a(z=nH;dcy16*Z#3iJx_XBSNOwj@m;#8mIw^F zJU7avB(E=N8EhBSdIQqAQ9F$GLZ{j#V!%^f>D;mLCL9%2u%Eek--o$Lk>e$2Z8eMH zW_njFL)zXS2no9z&Jrp3fjK?Its<(@GZ}U~TC?HsA?eZ+zpQ>kN34{bZEhAp+9>Vj zT8P?C2zf8`l{;vUf3>^4K2S8rCnJIL>PYon$#rstkr%?}KJn_Egv(4vu(kP&_%-BR z>QsRHQ?UOuU7P7N#K0$)h`>Z4(gstl1G{wC(cJ^m`3D>Kg-@OX3PUDZ}^Nmhy-pP(Quk`&%%g9QR>Z;H{f0H4*Gc$1| zWM=WlvLTONyKL(~NesLzv!|MIi-0v~rDgXemW})AB%1hjztzy4Uc&m_q@r1bNuL=V zLG!;(QThAUT;xhdwHePjos;Z_jUPks^vZ(2f{WVCmtg%RAP}>Cw48AMK98;#XAE^RZFxV< zPi37XdAT3*Wb}*~I@@lH$99s44}w?}RaVYwQJFZ~<#n9jWWlJ?r|}5?Fp=5U)lN>< z0~1&zL7Y4FU%t}owV7Z0TMTWUIuq{sStu{SW6`n}-D(mtqmgK$)iwUaC0&+qd1Fnw zM?{m@YKE@XukQM#<}KxPPAh4c4vD8?K`~?#HR&1}aYsrV+8q9|g`Gx_F6oD< zZI6L1JeiUb4H!>~WQn27(?XKa0oC=HrS^W{((uzDvy_kXt*r>Q4XXrOH#= z!dZjhc;I&SpC$6k$G=|`;~0s!bk36_>Kpxdt{Vt$oI%&iI{oLv+zVe7*p?G zgP8-A?3{RIG5Fdip``)Jmcjtmj>};>LK`=X1wxLVI6DM3vB}c zi0@Aqwz|4x)4bvz?i~Fy`j|P-4zKs|f+|acexz~EySFb%X{s>CcXP3c_Eb2&FcZ`z zj7U~GG{v{Pu73kIZJyt?PxxHlSe{H8t`DOX=EUm)Y=&4W7gt?A_~Ab|FU$|ucLLVL z2fz`%!V7l(6Bot)niO^#h`)W^=u5!<+CHxE{`PlUvgwZh{~~A5{vR2Thf)PuA;YMa zs}ES;nSe{{`|I`%e?a!uovjs*UVR3_BX>Y-=NbQydXj|?8d-fu#5-)r(4wj z;STqI_2aLU>)(md@!<~q8Qjt!R;5qigaL5d5%gEOzrr6DW5@rTcm6oh*~u{_X@*{H zn3fxRW6UV+y)RN71NFvt9a&Q)+>$QSAG4(xj7ck)6SoJ{^xSu8qxuNeW1El<2VIq^ z4r#UbEmNd70`Z%#9mF7lsu3#{s)Lw3-y{3Ov+W7$$DHk2P102I4ac{iGFQz0*!ra# zI-V19Ie-t&`rLJBDoOqEFjB9;9`RkL1H@FScFNxM85rewE_XHztgM3j6f_tgY?vQR zI9IwSGt41{x1zMi^57&yMJY(S=JX0{Z@&HPq!O()A^%7~J?5N&qJ83B7B_GDPA$8v zKEuj(srD!R4~vlg4pJ&Py-|U{+^7w|n`oA%U<|cVz8!$1&04B3rbg8b+uttwVd_T1 z@jhNYcsLAYTXzmRU`O7%Ayr%5+p08Bv6-SL6K+(_A8MqTuVy^>&6BbHP7FszG-)VllrD^9-A*=zl`YedYKf14teXY!R^=j=tCOo3r4Oz z7;ZIRbCxV|p-l?8YVX@8X)Uvwoi72Oc^Dq@l_WF!s6=+2Jj=AJ4R(;Ssysh^EKaZ8 zK0>tBOI=0BR-OLI$ueTT$+#1LqT_r3};2f*1K3{Z}nCCU2ToSy&4Ul^I`?_ zr-rLQdOW_=wHlIc=xP1HYoGi=CWa~*m^kMzHk|Ey=@OQW>|-wlo2&M{gt|eTR-I%k z>Nf9e-}C5woO>P`|Am|~m|CQ1?%dz-Np_^I#E&sWK9(l0Un}accSQ$z^%ynXu=`xh zQG}Yq%BDVZBXW{bV7Os_`O?^Q;@%Y1bVt{@4CcV{`ZC0?Jq-r-ZQI6M87~Q6a$IdX zwr0Mm3$Y!EO8B)m*7M7gLy|hxC;V)X%Sw1_SHZL0t0BYBg@&2;Wt{J!lqw8s1lhwj z%Ff=H`>xUSw2PChqDkh~N1Ht^o5|dr1n|Xnm(Mgusn%Z7*`Q_`+Pypt3o8hk|KNID z_HrNo94S?3dnJrqJiWQQcwlk99pNWL>}2~w8&B5D*`Elw5$)DM_P@?V55*TAc$?^{ zoe}|M*XJoEi12M8_f$*I_}#muzciDw`gZQ6LufVW({N!r$grE^-hv7qE4sf zIj4VrS8I*i6n##%b76cwf3o>t-@ChJR9MKZbl+=H1hiL2wUlO(Tm*f!bnxBjR(znt z++6Ns&oM<8=f0>oSD2&o)_{jV2NOe(;x0H!P00{4m~K31xmrb{2?!W|$zua8iQjP( zn8TPwEXk!&SODYW;72vcN{bG&f2pVbF09I_`BHtSWXaM3_}h3xiw;ki*-FSPWKykheU;iU#^q9M{u){n?{Fl}))0l+N9f}YE5raH)sJ^m!S zIjR&A*ahjhyi)H}L8A=@4W?|!PWdg)NGiE(sI4#YMW)>~fhSuY3u+xg7Lwk|5}S`% zYdiJ4LFeLK`lyIu=AwSmJL;3pKW6~&JVKt{A6Zgzw>P^ z?he6IS0I0oZGvr4C+beiY8@iI)l2>HS=Q{QT@sD{m*?2F*(9jU@tc3d@rBaT2Yt=Q z9Xqze)nunR^~$11#VpcxC(HZOPEGL`+FCu&-}Vnz@dJ+P8RvMl|334;iHp(xe?96R zR6oW3G%-`SD z2GgqiU`X)XA(KZuz_Wr+yF8@kEMgzxr{K`z{axm{z7#f<6h_Sb=*>}woF*_ zpJ_FY_snDK&WJa{MOWBRXJ>SOqNj^Z>R#uA#JU9gtiBTL*?=+M`9tW|g<&15qs||n z2z0Fd0Nm*gmoCaU-plUgmfraS%D5B5^J{=5wopp81$C|ue~hLeWxw@4CoPa%?5=^4 zf(-lF=V$lT!U!iYR@T;Zfc>L39J6l&Ku+<39b9&1!2Iq6i2Kao(Lam8kqM0MkyCH~ zv7RrtNwl%bgbc@o5B-3;)Mw_4%66{ZliA#GTK`@!otT+rebPFmF@e$E7>2&dl26E` zRPM!Wmgf&L7iZHHw?3_pIylu@vuLR+8PVF;hne6`+jtz;{O=E~b%<|RkSF)j zf)FJ+T4*B>RSG4}>XVDk2CO*e^`&uFPfsGZbloY?!?@ZEemVUkw{$KJrv$W5 zNIhlN;=mW+@_uT}vHSNz$LC9{6vs1#leW_^=oWeQ(iL@+)Q-);lni4eFeqx&J8#vA z8d5?cYR>6oe>t|-HaX-NCA9zz_j+Bk3b#eO(nL4Qjc@r%lijPJeo~ zeZ6rnsX3P2wHivISzWHC>f3f46-y<#%oAsi9qwF~hUCHf(PjR#ClXVn?X!>B1)pv@ z8RqT=h5F5uH~oZfUNb`m7u zL-|VL+4>s28rrLMFv4(7Y&lD2g;}2+|G>+hlw6V!Z*~c@v9u-j+cBs9M!Ia+RfiOG zz3@SV-s;2^q`=-BfTUCq-MS&k^?s}4C|C7=IGz)x6dUhFW>Q0^?DxIxeeZlUQ(&`E zgY_`)MmG5aB8Ksy(S7jwkBPa-lv!8SheI8v>M;Fz|JGvF%`Z{zA?4v`yguT5E`}~P zeB67r0YuJE{fk%I`&j)LC0*!Nh`qVKn#d<&A*n%QCZkAWGjNfw&a3OGjS_2S84vf3 zLN0#S4|BRX)mlLL&$}~bALXH%B9E3z&>V=CarJtIb6n;^p1-4cT+B~@2htMH=_O)% zV!61OG4mXB++L)hN}wsvEbl+A<^Md5y1kuk#~{2fTdOLu+(ZhBe8V-MAe|afwNquY zi_~v~bJqYnl~;fH_M3-8L9}FqOKjKMPqYLgCPm)Tegh}0SwI&0I)V%6X_Mork!rQ- z$pzV>7FW(E9u#xJ)jZF)lZoc~t}Pok#*91fdS3T*wM@b}9HnA(^i;HyuMhaO7`1#7YAR@E)^=dmi4-O^o2JaZ3dZL(~w#!MtA65>jc)p z%yYmzB#g0!7qq&_06AfA^`w;ih>ro28gOTjLREB2OWMHneD$DF0#A-Jz0ys4A;zky zx07V=c7g1##}~4v`3fyNrw%jQI8?0XMk4lJEy3LLxSP>8=GFFfv}QXq z*J`hAJ<-jb1nqHzfz)6<>QZa|fm?fQQ=KFgovw@rl-9jbyEE$z?{S=a-omU9i%YXm zF4&?EU~SX)r7F5iC62BSTc)~>4SehYTT-#bicL*Qi}$Q?v9~9xcI44LmZmQtXp9$4qtg8(R@FaiWQz>TgRU0rsxk2nLSYoah zs*bwpxb+CbwYaJ85q92_eR|TK-_Wg5(V5jQ{0yC5aWA0H5kvE^KG{~+O)L0bdT;l_ zOO^ds{S!f1NvDxdymKv;n)An-o5RsR&E(!yIG3e$ofR!Xu z77#XatB3QD)?Q5Wag5?jvdM7L0De6|X*`)>dQ0%A`sR1KFDn)Qd~T@LWvIrU+E0S1 zXDE1}Y#ObdHG5F{uUi7`{qI9nWySZHNd-$r?jPZ4@^U3U>&kO((HgAy6XqhqnJ>qY*Z%a`nV>ZfUt_Y|NO8T4HQ=HPYq8NV8y4pk1qZ2y>H9Ld0 zX0*1gMW^MNw}1#^Yb^Ywr5n;FBG!=IP6-U1Sz6>w>$Jr~vVN?C_M&lwo{+)Nfoe?n zIw=t&Hic#0j!@Uw_?DmtEePM5UlL+}OE2jxELC96MeTmmrBTZz zLoqG~t3h>0(W7@|?MovEW|5;6v8~7in|Ig^;-i^Szk~tnYpFxg64VBp=V*nW zQmf+}uW+`!37&%1uT>|*C%OA-0<5>b=c8#Aht+M1k3@{p2w#@QHCjQQCpjl!E-hfX z=Ul^rUwfw^t;;0Zzn7A4kU=lA4EnJl4Li+fuN6rRAI-tBw{GOEkGH~8L?X6g2Gtnx zgNpOnxyRe^ClO8WW;56?$Kq82s}~L9YtPyf#xwF>trQiQQxgrt<*H$Hp$eb~!b9^} zDbO$rd6~IRgQsY3%~F!iT2~)kT?CQcb2=dPWG|v~OC1O(ZruGeoH}XGnh#;rLYK-0 z2rod~x9)87Je+NuA{i!E;WCu&HMA9{%A9E|wZRov3VJ=N@N5C)5)DT`ucW0+V@fOb zcMVml3BBv>S=;)Srs6DJ^0f7r_VmSSY_;!6q<)snyRXh6KOV)}g80FZ>)22zjUEw75KusRh!$EDB2bEeNTTbe z2oRzoU1)-|LV%!pnov-e(WuQlgf^Ld`R zP6s)MxjDLy52};Q`)=33G-@F?MX9Sey!^W~ICt&_u2RG*I_EY#o*%uoT-OP&-=;WE zdnt|0Kj&5@N848kfNGOj>SZgB2Yc*;G##Z^AqHMs&jpidjb1}mk{;gdtEk1Ps`SBn zoZIojlBh9Yr`@spp{WpZX(ntWi_Ixj`1+l)B+=ND;}1sidP@b7eaLfP81)Ds5QA58 zt7M3sI;qwE{3^Gjf{4~Q!JTpW?g(c2sdDj3)(*wr!Tye)x)lL+^LKtQV4joC`a>_LR%GGMl4CZ6SIRA9e^&^hA9v3 z@>Sck0TFh_j{R|I&})-vtr+Ek$JZ6M;F$K4xF5}Sk$8>O$n>MmDJt?tVP4GsR{>CF zt-;7Xqv7&)Cgj|9@*+q6CNE_;A=562pB#cd=UkcEzYe&^E*73!-()bKyuh{A|;G3A6EEDzXorU>Kya6{~S8tiE}eidyzY*~ow z0(OG9iI>-1qTi#S+~D`?k=%Tm?OM+02{xT{eNOFcMONw*Mi}nw$H3Oj=sFYUGlpyY z?BhA9Q;;)A6wP|D?VZp?7O0P5YS`Ed5Z(a#i3xBnuQz{EI_m`kUVTM2(NheO8CsC< zN@{lp+fQ2TF`6wZ4=%f_89;KaiVE8p81x;_RE2;D5ZA|nWNztYTE3<4hLadIpM$gm z|7c}fDOP7`Uj(p9xci4TgfUAuBPY3FfE0&cq8EiJGjSR;c4PD^G;<&K z?OY#FK#sm3L6Z}jE~BeLv2)Dc1xxM5XHAV1^lDyV0{v{$Z8ENp-vbBN%>V7Hty2tT zl&M+L{DV|G^CPXwm{^hUMQf^8@{p`zZiM|HF+7mux4&i~Gxy6cuVc<$odZLtlOlFG zcoa?-(N|-GiI4ETU_fPnq)mE$Mg8@+A52&JsZ{Ujw@*(KE7IT8gM( zMdWB`^mL(|HEl(6h{$aR<2l27*4m|yj9SeFcRA`Is$QN84KE&&&6~~9kgI6lNT({( zDkW*xcUTb4o+-}d<-3XtwWnr@qv?q)aevQnk8BR24VbO;AB>(UNn7uODUMvF>qFqvs3Pp`o4I!&W3MS81H(WOdN>iAM9ap1Pabq9--j3Oa!WIz*+} zcRddBX5mR(GY-rfnqHA}(5)b6M|D$c21dSuB)W~80yE>IUMIY49$r5Vb-fv?(G2ST zX6o@i&!XCW4QL2k@`8`1%ez1 zMm798oHL_54EUat_27ahBNl>@0JQr!TQpj4n?&9|5@CU*X6Z=VV_$~@dGRa;nC81}8yOZQ=LV*CUV zY#s>Z8U6z95zE4MUcp3tcz-L+;^qG5zyXzC#;Ze%O9{9)Jy7PfRaPli3G8YQ5M!x@ zy(9miAZ(Y4{}X%+3>8oims#W7$-Ve8%-ye~a2`-vqs$4{KQI9L`6^hqO5IQgV4&d@ zhztT94RIjF-A>fet;-;e!3s?brBgyhYO5sZ|qExIl_-3Tf0FvP!Q7r9{mkk57 z&9NFkL=j^RzsVMFz5>pU3?ZwcDLawj5IQIfnt0GN1$WgQvahVy2i5ZfgUB40bq|-g?GL}0oNKwJ8J2gQTKW#VtM-NPsQx(|u zPiB_>FgQbH$X)k1G{2i7t$1$}wW;y2Sa&}+5)Y5zsbO+qqRHQ;-;yo;(Cf#+UTEpF zl;vAvr7=WcI`@l3MVnl)5R+0hD;mJXkz1G6pcMjSPUSpekI>BPLx}+Z5eCQj546oK zAK(1dISW{0sk=5B4k5drRuJy>q2!TNuqXETx zo9upZ9|j6dpgwQt9NOG50A!#gihz;#JzGn5a-PCV<$ln&xXPsNoRsf zBcRA0@XX@LdKvZWfgKl$#?yJNf>C@ zj@djJk1z*IlLAIPAl`t;mEV^|HcEyGmimxofB$SVF2DAecpf$y+RFU+6y+$R)?7LU z86&xS!qX*gJjiUX6jlH;HmRPWX4REo9J++-FbXwxlas(P156?$b_`viAvHISRtG*qI7eUSn3AN0%!~sly6yfLo~Yi?-BM? zSyn~N5xi$*>hjRr27+a-&sJz&Mlz??93!fe=E9p5P& zEH}|%)&-=hcx1+)<5ue#gPp)}|@fyhB_xKEl>hVcikfoLp#x57Hac z#4CN?g`V!Ht~a?La419{ZinK58f+M}c)Ewd*o#)hA{dFEeFvIVP|ySUCoch?V-<>l z)5a@mz{-gS8Ke(J!bN4-bIJ2&fOw8J@#jc87ZZv4q64cO6rCvHTS(LtSf=uk+g zL4TS9QigZIH6$PY3()b+|Lup*KwT|(Hy$g9g_0-C{(!m_LNF7`l`xMfTp+;KZ=MtI zUYDZ_!&p1t{sWJG&unSdMR4HBKGsZQ$zA^j5>X23`d<8JzfC*Q`G}a?$=FmTF6ECP zXWKqyE>nIHR%Q!|!7 zquMr1f#e=hiJ&yziF7K)(_&Ff#t6D{p{B!RdF0ezmN7ti0k|K z&GFTu7kA_xjY1#o;jZ(}AiV;)+jQY`okvYBmX%17Mx}rjRO${E#H6X-4#eAcc2gVs z8&g1FoyLM(6%;LpfludNbS*(ElcjmVDJX#?`5gFGuDyoeJMFp^Td>1RFJ|gZtS42F zYPpx?p-*slB@k`=v?sTuthP4XrfsP;6+AM7@H33fA!2?rd|7 z@g?({KTmaF5OGz)JLQ{{v_we~ z+tjUPLNAaH`@&92L3fg z1#DhOL)i)JpueQm|B@2xbI#m~vM z;=uMLnp*S+ldMHAhHLa{1!>t%P-MqgWoO$Z{X&#Y>NavtE3*9VdrsA1>(uj45wo&YC;&<3zg+>AGWm)Y2@Et`_WQ zT8ri(9-ix(v2p4a97lQ{yn*?++3)2iTDb#=6-5|#hq`VwKeZiz{tME|!ds>%>W*uO zT5kUak5aN(C2S&&Eg!<_p};kH5Rrbe%XBok`o14G6IkvN{-r@V;E?PCH7sCS;#giaB~iWjIAs zn~)sAD#4m%_mj1Yo%X_HH=-(Ix#^=~Tv^IJYu?LUoFS4E08&)s5tf5Ev}tuq?)@|b zmpZBKpe31@h9OE|pqVap>ZUbWrTFGY7g=n14y76a!jFxeS?-u~3(t~SLmg+{~}4%YNMdYu8RM_as(Tk>$OhH|3ybd7|^jbb*gRLh#kP_ zEOgH7P}Q7!E*T#$f2)aEnV~+Gp|Wp*uxlDxH++j8f506#!G)FDCn~wR#cfeEJ(WN%olDcq%v}ij6G4oZ3e2b2W890Gw+pUuo97)n%!=h8UoWy`5|o^ErqoA zmgzFp^(>UmB!uz3Fj`Wzv9kw6E*d8%b^?d+CbW|T=mqi6rC!v#psc>8$xKB z1&RJY-|N0_npuL)PW9fe4^caOT4cXV6QH#=qh<`WI^`(q*T+}iSFiRwA?>{F+b$Q@ zeS*>T6)gJLJp+#i*1qsxxE&(nm9!4fU8}5YKVP-PV&9RJ3%Pk-C_><9=DlX@Q0Zk! zoDgL;a@mY)wcY@Y9XIp&Dk#x%7LL^>E%VAj!kOxAm7;5l@n}4@vH^u4&dU=YVk2KO z3Q()_zy?j|U%$9-Bj}ge+pDbK&o}>4!}ai;QQKC$!n!SOvXrnndxxv-ASz=284G_~ zA&**qa##6;X_I-lMZWnH&9j<+6j$sgc+JH#7=$zIt@pa0W8EHH_03HU$jHsq;F3>x z+Nu9h;^7`1(88)Gt$o#`8`#hA6~+g3QEV}3dwb#jt;|Mc_l)-ezN`3nH2Xx_pZ~ONv!5<|j{%9ridQ9UYuN)-vz^`=xQ?nbli#Ja1SfF(6fpj(Q-h{vvvWf-DU#Ss zM#m&_?nj|Hg4HR5$jdSey#~>Gqs9oqR+r?f?^NtARP<8RtQ?IX(Py8F+l zf!Jh=gu46k{TU!qZP2o@x1}v4QgUK>^epsZK3$!%4g#;tK6{wA!)-f?nuqz*FeRuR zkLvs&qRT)p|I_gJ8t_8fXNTzuto@^#7jv zpbk{Omm6cF-J{;$Qb5hUfN=g7tx7JWucfZwYHDf<3JVWg^@5~|^nU$vkkSFUzJmb4 z{aG4T?Hc@D;~tM0Mr4=sqqJ(NcYQpG=I2YO%4yRaBCN- z(SPBQLrqP21}-7YKAN9lbjU%+ugm7OC8BTA-gXYLOI0A}Ad2+9rU)oTo9F266~p|^pHNR3|v*|sZ` z03dLaM?@Z(gV$lM?(0@O|DyD}i_VXPKRYyXQ56vIxewtD{{-GG#DM7i(LT-W+_>iS zH$qiS&AXcUx$pa&4g(x_;k@n={=8GcajQvY^9-Xo6_8uQd}#hZ!s(CIqe6$xO=>=l z-5-8R9SQ=fH7D=uZZ0S0diA=Ma~}&eh8!G~v_X?^cZ$ZRs%ECB{>%@vR@T;HdXe9S zVfo5uFLkpnOot%`OhS;$lj#an&%{*^xXhy6H+#4#L5hFwC zj8yR8!TnR`esE||?iit)J2lGIdNn?Jt@~7WiH#T&pP-SB2NQXWGi0Zp74IMSFciGba7=9LdHE!Sf3dK z-wpNjJ}l8lT=l^n?Mygj``|k(ER>c2(M~y**Ha=I`%E_#JuZ3NU=Y z6)@?TA8SjZHM^O>vPL3pZ%U{b+aEL6syOKqWpwb6aoOX!%v%A&W^-EamPPR8{|*77JuYIz>e>SgREcHNVASE{Xa&vdO; zopsrVVAhYs&(h3bN65*y{6FAt{>;uD3FjG^!UBqEM=L98(+>}MHv>#&?7|{RLm&-J zjFWf}wVxc2&n?-N@X8VHcH7aGQd$M4WoMS$bo3s7LI??VC>pyU-2>^TcD{chL%%r4s`PuZ2jOIQbI(N=uF``eny$=IQidg{x?B%pQ0rubQ)-|?o zEm)`n>coKUKLpI`=yf^MFGy#`pX=;D-aG_973K^7gH1bnJ16b&3h0Y-^H)~xWu2E! znGM!L$t*zk3wNvZGwF8Tr3;7~dg-^iAriqju5lVxfHt+-=eyg2KN9NaPb#Uc{Najw zA75#?D{Uwji(gWnyA9dZ)3c*`LORjTZ@r-J7z>uo2EDCu}e-4omke8Ugg-UD6;sCN0TU!F3JP5 z_-m1t(E2yO=$%HNIYe@W#x-bF~a4Us=YnFc!!m}>M`+rTu0Atcbwbs z-qAD%e=Wdb09FTArqSSDuM=SX#pIuQcaMCXR*|?@<>rjem0!QZ<>Yj{?v6Y$xxYH_ ztPb4EC14^!eFz;fXwzIeOigcUz7e(pMuESjE7q?pbx&8G(=-8sNcL#>AOrG7f#!9) zX1z7HmAO52Kw~MK8I%BKb4Z3WxSL4>TTsbE+x0F+Ks*m~lI5YWu7`xPV^seSN$`5@ zJP*QfnVU7x0@L@_BG*0*mk2Oiq`VW-s;yFXIM-Lj0-Dhg!io7F+q#=SJ)^rHtOsy- zM^pVzSL(09A3q`Xd}r+Z;v-O{q6&k6dUcvoWn|OUb&KBsy0?kS>TJYZjgKU+u=BC_ zvK*iwetkRuOh!l8rBE5g0l=gnpxpt7$uSL&i@VufS5m`(iUquQ_k>hbS@}+>E{dA~ zE2K_1IXvst3`AJ&b0p$ZwJwh7*Wmxq64p3u&Cn?4=mpNhRD@7tOL=JROZ9c-I!F$+ zToW6+r%=+?W?fzQ%6b+(QWjStov|9O$q}DAW#9bSWZeNK>qq-R9uhDp0s@}WK{ANx zPzBV`*{|d;J5z^ykH6uR?^%FU(T1Krj?yI>_}|@+Q^+U6FN}(3wq)tUnmu=`|G};( zzdl6A0OBMz%TFV54;Io>hn;Fg{Me^g;@Qjt-%tGz4d?^4<(i?7#}>vx#f@Jb)98EG zWZG}kIG7NLe`t+%@Tn7KfGegrxaX(QpSA=a?+gt1b}SO^&F-muPG~qBK5Y(Z*qQMB z4#v{ZQyx0lJ`(0jyL4$#XXC_fjR@`f1`>Hkw3W+Cmr>swYCuUX=GMve1Xq1ZDoqn| za2El`UrOO;i9s9yEGo{Fp7-f}ExRvo8>0Dd@9d`a46GFDzutk|i^Tz-Yl9q2!&^80 zU8899axCBf@gD9Ug!H=RV<%A8x40U{J6O@9#0RZjgc02y{}jcUA?Py5F&$_A~DAf@x=M;igD4#~@>DDB~*azvgfesFd^!E^rJPgnA&Y<)Sb zmFt>re`&!XfTO-;bx zGvd<+q1O}g4K(|H9ouVHdEG&xT^O(e@$7C0%nexYjC%`Q;bp`^jgvWqN*n#zEs0uW zc7m5GIsR*%*UN*C0-VHcTcuA9$@Nk&EKU0~v5VxUq!fU3!v z^9{>Z&rtTtZj80^F-UIHl-0DJ^hRqG%hs+WZys0@wH}JD3%jO#!e(D)+*4KozUgD{ zxgTYTa(44;<|{BW_PJ=lu0NvX0nuIe>(m#n#y`%?M4Q2 z9x#5PG|n~d`S62+$X{-+0FwWhx|(dmRgYN1S>>F}t7*Hoxqi|)i7weI)ry3Ejb)N=ZyzWzWiM-XZLhublFt_h9(Y_gGkgc7*Fw5B;;i=tx|xdjL6Bg z{Uj-SMBNB$}}qvUw)!MIG9zk5=c)r=Ec5>>t`*k1{qE^|aOj=bnB)Is==Z zn0~hnoosp^m$wp@UDSDQGSSN2IlDxTkv}Lqp3Sr~&u21XF?BFTBSXKVSeW4&*^z#i zTzsTM8Fj!zX}S7b{lrYCAmZ`0^>o?8CH3mC*g)qZp?7mjVsKkCF|dt<)apo3%kUm- zn~yecmnaTa_5-Dxcl3rYqkD^{0qgewrA~hD?Z0Nu|Cu*Wcoq7Wym{e{?+8ahIWH$) zcabqdPP2n|25T)=q^Ej_4`d|O9h}v!J=Ec3p0AuVe&0_nwlPGRKg>6!_<8CF1={v? z8TdFZeM{m|2xN(gloDW}Ip8fTq>YljLsZX^jtNnb)?LkVtlYp~`Yf7#X{vw+=(>Tt zPK=FHJr{%V-)MwoB+2J`g|yg#lLh_WbGHtq1h!T0PYq<`bFA_iYk~Q&C&-Q5mBLsb zul2Z;|3>##b$l#In-F8p%glY}yGo$ZJx21qT=K_i5053yr*6#R&Z70F;#bYH?`CBJ zc|>YZD}@*WUYC!vUq`jBsJE_$6Z9@Kxsb*$^o!B4a%76O?7x6md?tOQLi^oXYJG35 z_vuD{-s}a~GfC2V$n~8;#VW=?mc8k!J1v~v+B;=Tk=J>Lph8u`Vu)*BC@5IX8e;x7VCTI~R_-{S*m$2d>h}GW z!9nJ-N=Mvz{!#ip8f#cZXgg!y7$zIzyC%0E1hn3y)l>+PY0n}*hA7Ho<|2;FP>rO9 zS&+Mqp`7ce!G1CGt)8ZH_TVXH+PhO}0V!Vyx2bh$R6lu=w*w_OFPtiaLQ;}v#s28>Mc zOj@-N79QvCh*9%s@O2nRF0KR8(ES2Xnn}9-X4Xc@YoR}ySD&?bb4Q8%31cA<6sfR{ zJ{DjQ)Y-D5ENm6Nz3(#Vriz7mY@C0U-cXY0r+HEP&FCwl&b#DrTYbLhlGesa=FIQr za_$+l<+1z0=kn|cG=lG!av1)T3sB_(sfhszZaRoK?Q?YP!foPsdLrzYX;XLYj~=57 zgyk|D`>^2er@`xy;pI}8{PH3X_Bu$z&rxGLnlc+N-y@rc)cH}zQBm-go7 z&xMb; zt;RBPFOkqG(%G5YSng?*wkNTR!Ij->R*bGbymCkR84v1n8BR@yf|22_N*HV`zu~xt zYmcMODF58V2ii)qW1xgRzOeC+>O5c0$=aDVJpZRA9#V<*a&>*X=Z+0!-Pxt4`oBbj zfK&l>AtV|Eo%rd$e^nCxfA-^KYO(#(x5x^(X;PAf*4!=(9Dk0e5LQ2(qOR2{vU4x>Z0xfaFw}Qxb|E;5S!W zEo%FJA;r%iNY!#6rk+&1z9evgnyPYsal4elPWWGGc+U~+Zt6j)8^RU|dvE4{S{&e+ zDoB%h(C-gxrtJzQ0HKhHkquni1t>~EnW{+F@IN7?8$b}^i;kY0eOrpyv59gzR-b#75}D9nr^Zm7K{UFB_!V` zNl1H!Hvq5hW!w;em~~il7v9ms#jo{pO`cqH*z>IFq~Km@ECU*AHw-es@GE`$q#YzE ztz1hRT}_Rk%8ZVnPU2G^R>0HBUp!M3U8*u2EHUY|{6|=DtW18UgJzj)HpK@Gh8Mk7oW^QgKk5q$Ol~od1Rja&Z5U86~Q+ zM$RA$4q7$kLHq(ZU8}QZVApo~cy!!8?cnX|89uL>T|lL36I!X&*FwA%2@MuJL<}1GjisoJS`+II0HwG!_8M9-~Nt+|fkmr_gMY`yDO zwSc~CXF`%TJ*XY3zb1&RsNP3Sa;{AuYnuPv47}RmrJnna@n-E8~ z_+58SUC{(xvR$_#ypz95_KEdmC0XgPlFBL3Z{}MeRlr4s;!x)O*Xv`Wjt3i$ErkUP z63cSkZm`b)ihmj((LSn|y@M%m{#(I9yf(%t>vHcM3yv@A4=JsgG!zBZlxryyaH z%YA}+U(k`Qc;B&yD~T-;ZV1v zreUnPeyDg}1vLTG$4!vWEAhM#H3kGu_$?h!UWru>KeDycLe*y`^TRCR0EsYJa@dLm z(xXZLqx9&J?M4Bug(b|Q4|UhRiL;@jdJLT05pj@uCADgjtx1)nC%1G6`IIKLK?C6pL#OH?DwM03bXD|^vlFFD_!1341fpOicdup4%y+f0=_^yx;O+seuBI% zu9LQTzcP{rKj`IrOOo-ukg-u$_j(quQBBx=Ea?}E<0qlIg7OzLzPJf`Yjo5q^U||l z-1HA;=~q?Qgq%s6Z8V-Ox;8L355EI)wc9HQ)|$ysvB|0@>-bW80jL{lla&qjuG74i z15|8}y^+(x+SscS71O9qC>*taUMIlG2ObaHE`V_q%YkJBf#ccLD?l5LEq5DpCGl2J zX(&mQ7!R#Ce=WLXl%q0yAFt8D{&_0>TELhLr@-XLKk7Jps;v7ADdk(jk3V;NLHPBp z%k?dJ<(HPPunNf+h|nzZ7|tzXu^`j8b003Sdrly?*IjM^i;5ep4C?MH+um9ZVj{pG zYc>kM2x>XM<44=DlAIj|8#i`WU-OYb*W$RH$_ASyZ6v+00yeVS1%!q995My2x3Y zv{ud8aTjQMmxj^5WE_;gPw;2+32$zpNCpEXpel4z-A{kIJe$yTv?SvH2&yMkA+9@J zasjfmaAioD$C% zBGD^r}ijn7__roy!~Gn~r}^@&<UniX>!VMiDE7`aE~~8 z`-=87Um<)j#5)xL2|LPJxH+SatC;-!x~>01OVMyJ z|EGoM{@R$&PkJ&%QgRO;;3Rq4JOfKm4zIV+Izu7!&G&^+0Bu+csf3mVomsjjoe@4C z-rNBgmf?L6drO7;u)Fgh!3rU0ESp0ovY8nkdqs_R>cXc}h-40Y8i@U3mOnlK0?)z# zr+cqJzsRTpoxvlh#K5D`O1F9*RRiY*B!bbRbv8mk5t)leV!cF@>qPHXt%2g;^h0%X z)cWLTBeC6>25CkdjY=|NWrD2zAxov`0`)5(y1)6R@is-&=MJttz8jQZ=mhs%degTb z@z#xw7~paE&tP%Ga^7NLd{pzvWD zXo~L=t1GK2#3|c=ToLwd@NX=e;Esp4>Z^eEDUi`f2CGO=@$g)Ry*RMH(oHn$eg!t$ zix0@(Bw{KRwn|ML!-Lgu;CO?PfpBVg6yM;>)C+!LCxbek3JbrM1QSJ7V8Kmgz7&k` zNgWHa?jOM}d0#B{eBe6Cd;tQUn001`q&UlK6N1keD=4k6q1o0b5)~J6COJ2}*UIR_ zpNG^z!4|2g)K7$X(E4rQLF%gjGSRTf>sU`_<5A{62wRteJ36!}xw{#V#gwrD&I|mg z1?yMIt9xN>{r=|LBp|;=ux(#~Q(hjl{TI6n&IZSSSC9hN+P15jH7n%xSZY83z(mDi zsCv@Gfk>|w^ky=z|DKSSgOkGqeM}8a>wvB%G2Yv7adNr$XK7I`b?~?&;x#);$3ro< z*|ag*Ndn^>u*!I^+|WZg@zu6B7Ks!Oqpv|(sq3GsL*_@bvZTc=boFA70SBkmu9dGM zeMA|Uw6ZuCBf1F@(}*`R85i>ZCHhB;F!xgKrd6Vk6 zi^THvX`D`Fp%W{%z{!@A<;CtcC2y|BE}LlY9*hV9EoiW-G~ql`KwM18!%e9%1P(1L zrpE)cKIXHX$PYOYmcA#6DlP=(1`d`(#{iPz??@IO#!cUa+Rf)}ZpiVbbM(DKlV}zm z7?iCm<-{!G?bDFW3T@wRq$s$k{Ks;$*+ZCzbn^v*Lf?sDQ4YtsuinLWj3;PVDx)2a zb`eExew(%2YFXqmMrT_pt6k=HH+HKIJt z^|{UEu$i{p6TI5H?JU#fSrxSi4SamQ&PVt(0jSixr|G)v5AO1{D}}C@+3KvSYrb1^ zLj!}92d=hSrBs`bs10F5FY;y?z8lE6jA@NX6*D?pu)|Q@PPm555>07Xap%{Ux7=#~ z%?!zW5wFkmLN0mz%RtQfQ~K!EHXvlv*zlrGR7TG{X7e5d2e)S{jUfiMaND-^n&^s`-pVh; zS7gPtw#3eP5SZ(gp!5yk5xu+$G+W@utAO_E^)?tevlwx)<*xbhWb{h14*K1L=Jqc6 z)fF}`8OF$NPTtg#g`W$%n9-oyzM?_!MGZX-YavN0m;klSRHF2Fyr(OrP@5jXhlD>tSS z*IVle%xRUex;uC*wAM+KYRaJxz%+qNz>0@BLuT;0f*u#O|HBn3yAWaGN@^ z@XuvyouEs^+)SscH33CaiMtvd7(jO<^B$}!JlJFvx~|w!Xcip?Y^?s=Iol`~^OOSA zkcwqQmkDaV?Spb~r!x}n2Ri^JXFP<*0%L+Nj@zh@ov0J%+<}dyuLPr9THIoPs)QYE z|JDp4TuTM>`h>GsR>*scOoxYy`KEZXwXI@!piZf!Er+0CC&X%!X8t&o65h2<>thBZ zmh2I?v_kYfnG#+BSYn#o`8-p~BJ*fvre8H$K#;*DPrH3nXeTT~PN{j!3Uzql9^|g+OWilo}0aepv0Y|Q+k8j9TEvX!UtnI z^G`vc;#lb2U2kTArYP)%Toqs&x_7UNriXJE`!cG9&xp+O>J*Cb-6Clwxqj*eCR%GU z$QjEO7R3r#n;6>MSt7DgQ+Jy}T96Bo-I|y}$I>5oS?#lN8$5L7V|4J7ZscM$giOuiP@5TT6Hs@it z8m-~JF11^bhZRPssyd6c_aj>H6sqOkhKDO=jc z*%CQm@KzV73K|#;*$iBb68j`x=pDMJ55j&=5NmD zDN{G`q9S?I*C;~8v^%bC`2x3Y~v6=61ObBPF-e08%W0}^_?sM8s{ygxE9NW5mE zgZ3hZGdAN@a9%FSN?Bpech-wv1lv(6fZ1%Nr9nK68%?JnKNsS}oDdN-ET`5d8Q6Pj zZoe80l$cXQSCgXW--fp*QIAaQx}hK5Om#imM6biu zd^i)h`G|!amHl>g2LiTXPH0m&Z>UE=&p)$0B$#?UYuomaGLv;26A{RA9oH?697}7( zgihRq!imzRFLq#vif%dLA41^0-pZGakVbzOAD$kS z4g=QGIMPW8^O59=e-?HSoDe>L;}!+FWlbY{hWdV}V#UmqBBkwgJk|0Xgd8n z^}Rvhq?Oz=5g4(Cr=f3IDn?u!M|XCl*(-bv{e&t_tH(nZeKWVZfE2tHs>esJzD)%I z&uV~}wD<;WKv%R2g%1Wzlre_tPD6t3@44TLl*K)(0+5$35)R8JQC00Z z6J)W}+JR6i(9&^6^B|RPFBygW6c7ec;wkicN_E)B6J6zI>UKm^C!>ZO^soWAMKBrE z%q{>iClDwB0NM`IFGhNuQ+-tqwq%8V1d-A1!z$EK8MvmF^DH!4oAA+aT5UdnwY4`7}Sy5GI%|Upk8^~c}qnmTXO>n@QuHI3I3w6!N;r# zUYAlI4?RDu935t9zTjco>5yi>$6eeA4ZR=mw_Y-@KF1nxi#J7J=>KZQN3GF0g)3DbtP_!}TF>|VwLZY|Di%B>IK&tfGP(EC z4?lmgX7hd8?t|Yrs=oXB^fv`oC!c@&?wgV>$FDOEyqi|-eRugA{kpI5$9C@DbNc9+ zKN9<-53YLpY`&5{Xl|E=8lRg8ncp&zta?GT_wR{muj2M$hN)K=GoT-#V?r7H;pfwx zWb@PTsQWEg$NfrgMrSSdeSW%QKG&GRr{BC@vMG1vqB(ndf}O#Cn!)cepr@eY!11>{ zgE;LQvl;a-!Z8o?MgG&GA7?V^_d*i}950g{wKDpC^xZUH1m|JCs^x_%tt93>+ac?@ zxTweSnE(6V6aPF4eqIB=`ZqN_^O!r$oE|lZ`*i&q%E1v4C+>x{aBtn~;t)soOyuO| zE%$vc8;gJ#={V&8|Ed3rcohbk)juIu^uE zU_9U4qT2p>vyq9VUMAPl`E$(*sZObXeDS69um6uf{4p>vh*Eg)CDPDjx>&;Wk&@!J zRRt~!*47LwD=WiRLjl(K0*~Kw6-Y-U$1(EOo2T|M?=t?Gp0oEyqs^dp{&RzdBLn{U zCzmUVLQkK*ST=ASfBC8JKc8;*Y-IiUqk9p~FD~5;BIa~F{M_48_T?AV&lLByd*|7z zoh`heW&e*>oFo1G^!qL!UG=oHdO2O!_4ZABJbC_tBuu|8#dtqgfc^Q?gEbuLsM6cm h+2Qf&*BkN++Z_>V9+4GY_n=R#&e)!={OQW?{}%zAwKo6& literal 0 HcmV?d00001 diff --git a/surfsense_web/public/docs/connectors/clickup/clickup-api-settings.png b/surfsense_web/public/docs/connectors/clickup/clickup-api-settings.png new file mode 100644 index 0000000000000000000000000000000000000000..893458c618666231f23dc87709452ae8feaee714 GIT binary patch literal 84729 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 ?rgk+r4ulZg?k!}dz1maUbvx| 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 F{|8WV$@%~Q literal 0 HcmV?d00001 diff --git a/surfsense_web/public/docs/connectors/clickup/clickup-app-credentials.png b/surfsense_web/public/docs/connectors/clickup/clickup-app-credentials.png new file mode 100644 index 0000000000000000000000000000000000000000..9735c36b216ae1df946ab5dab72967d92847f7e6 GIT binary patch literal 101055 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 literal 0 HcmV?d00001 From 93dbb5355f44af316ec3c9df21acb82d59875826 Mon Sep 17 00:00:00 2001 From: "DESKTOP-RTLN3BA\\$punk" Date: Wed, 7 Jan 2026 20:56:48 -0800 Subject: [PATCH 74/75] docs: updated readmes --- README.md | 2 -- README.zh-CN.md | 2 -- 2 files changed, 4 deletions(-) diff --git a/README.md b/README.md index 03a7c88c1..4f2ce4332 100644 --- a/README.md +++ b/README.md @@ -194,8 +194,6 @@ docker run -d -p 3000:3000 -p 8000:8000 \ ``` > [!NOTE] -> - For Google OAuth, create credentials in the [Google Cloud Console](https://console.cloud.google.com/apis/credentials) -> - For Airtable connector, create an OAuth integration in the [Airtable Developer Hub](https://airtable.com/create/oauth) > - If deploying behind a reverse proxy with HTTPS, add `-e BACKEND_URL=https://api.yourdomain.com` After starting, access SurfSense at: diff --git a/README.zh-CN.md b/README.zh-CN.md index fcccddcaa..fe6ec8e30 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -201,8 +201,6 @@ docker run -d -p 3000:3000 -p 8000:8000 \ ``` > [!NOTE] -> - 对于 Google OAuth,请在 [Google Cloud Console](https://console.cloud.google.com/apis/credentials) 中创建凭据 -> - 对于 Airtable 连接器,请在 [Airtable 开发者中心](https://airtable.com/create/oauth) 中创建 OAuth 集成 > - 如果部署在带有 HTTPS 的反向代理后面,请添加 `-e BACKEND_URL=https://api.yourdomain.com` 启动后,访问 SurfSense: From f1328db94f5238baea26fb0455c11ec89fe4b55e Mon Sep 17 00:00:00 2001 From: "DESKTOP-RTLN3BA\\$punk" Date: Wed, 7 Jan 2026 20:59:54 -0800 Subject: [PATCH 75/75] feat: bumped version to 0.0.11 --- surfsense_backend/pyproject.toml | 2 +- surfsense_backend/uv.lock | 4 ++-- surfsense_browser_extension/package.json | 2 +- surfsense_web/package.json | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/surfsense_backend/pyproject.toml b/surfsense_backend/pyproject.toml index 099f1e338..e3e7583f8 100644 --- a/surfsense_backend/pyproject.toml +++ b/surfsense_backend/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "surf-new-backend" -version = "0.0.10" +version = "0.0.11" description = "SurfSense Backend" requires-python = ">=3.12" dependencies = [ diff --git a/surfsense_backend/uv.lock b/surfsense_backend/uv.lock index a6ef20cca..8ec09ddd9 100644 --- a/surfsense_backend/uv.lock +++ b/surfsense_backend/uv.lock @@ -6409,8 +6409,8 @@ wheels = [ [[package]] name = "surf-new-backend" -version = "0.0.10" -source = { virtual = "." } +version = "0.0.11" +source = { editable = "." } dependencies = [ { name = "alembic" }, { name = "asyncpg" }, diff --git a/surfsense_browser_extension/package.json b/surfsense_browser_extension/package.json index d7edcc95b..b225bc206 100644 --- a/surfsense_browser_extension/package.json +++ b/surfsense_browser_extension/package.json @@ -1,7 +1,7 @@ { "name": "surfsense_browser_extension", "displayName": "Surfsense Browser Extension", - "version": "0.0.10", + "version": "0.0.11", "description": "Extension to collect Browsing History for SurfSense.", "author": "https://github.com/MODSetter", "engines": { diff --git a/surfsense_web/package.json b/surfsense_web/package.json index ccb34b973..3c98c47e0 100644 --- a/surfsense_web/package.json +++ b/surfsense_web/package.json @@ -1,6 +1,6 @@ { "name": "surfsense_web", - "version": "0.0.10", + "version": "0.0.11", "private": true, "description": "SurfSense Frontend", "scripts": {