mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-29 19:35:20 +02:00
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.
This commit is contained in:
parent
431ea44b56
commit
0fe94bfcf3
12 changed files with 411 additions and 511 deletions
|
|
@ -54,6 +54,11 @@ NOTION_CLIENT_ID=your_notion_client_id
|
||||||
NOTION_CLIENT_SECRET=your_notion_client_secret
|
NOTION_CLIENT_SECRET=your_notion_client_secret
|
||||||
NOTION_REDIRECT_URI=http://localhost:8000/api/v1/auth/notion/connector/callback
|
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
|
# Embedding Model
|
||||||
# Examples:
|
# Examples:
|
||||||
# # Get sentence transformers embeddings
|
# # Get sentence transformers embeddings
|
||||||
|
|
|
||||||
|
|
@ -100,6 +100,11 @@ class Config:
|
||||||
LINEAR_CLIENT_SECRET = os.getenv("LINEAR_CLIENT_SECRET")
|
LINEAR_CLIENT_SECRET = os.getenv("LINEAR_CLIENT_SECRET")
|
||||||
LINEAR_REDIRECT_URI = os.getenv("LINEAR_REDIRECT_URI")
|
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
|
# LLM instances are now managed per-user through the LLMConfig system
|
||||||
# Legacy environment variables removed in favor of user-specific configurations
|
# Legacy environment variables removed in favor of user-specific configurations
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ from .podcasts_routes import router as podcasts_router
|
||||||
from .rbac_routes import router as rbac_router
|
from .rbac_routes import router as rbac_router
|
||||||
from .search_source_connectors_routes import router as search_source_connectors_router
|
from .search_source_connectors_routes import router as search_source_connectors_router
|
||||||
from .search_spaces_routes import router as search_spaces_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()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
@ -44,6 +45,7 @@ router.include_router(airtable_add_connector_router)
|
||||||
router.include_router(linear_add_connector_router)
|
router.include_router(linear_add_connector_router)
|
||||||
router.include_router(luma_add_connector_router)
|
router.include_router(luma_add_connector_router)
|
||||||
router.include_router(notion_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(new_llm_config_router) # LLM configs with prompt configuration
|
||||||
router.include_router(logs_router)
|
router.include_router(logs_router)
|
||||||
router.include_router(circleback_webhook_router) # Circleback meeting webhooks
|
router.include_router(circleback_webhook_router) # Circleback meeting webhooks
|
||||||
|
|
|
||||||
336
surfsense_backend/app/routes/slack_add_connector_route.py
Normal file
336
surfsense_backend/app/routes/slack_add_connector_route.py
Normal file
|
|
@ -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
|
||||||
|
|
@ -31,7 +31,7 @@ class SearchSourceConnectorBase(BaseModel):
|
||||||
@model_validator(mode="after")
|
@model_validator(mode="after")
|
||||||
def validate_periodic_indexing(self):
|
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).
|
Supported frequencies: Any positive integer (in minutes).
|
||||||
Common values: 5, 15, 60 (1 hour), 360 (6 hours), 720 (12 hours), 1440 (daily), etc.
|
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.
|
The schedule checker will handle any frequency >= 1 minute.
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ from app.utils.document_converters import (
|
||||||
generate_content_hash,
|
generate_content_hash,
|
||||||
generate_unique_identifier_hash,
|
generate_unique_identifier_hash,
|
||||||
)
|
)
|
||||||
|
from app.utils.oauth_security import TokenEncryption
|
||||||
|
|
||||||
from .base import (
|
from .base import (
|
||||||
build_document_metadata_markdown,
|
build_document_metadata_markdown,
|
||||||
|
|
@ -93,7 +94,10 @@ async def index_slack_messages(
|
||||||
)
|
)
|
||||||
|
|
||||||
# Get the Slack token from the connector config
|
# 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:
|
if not slack_token:
|
||||||
await task_logger.log_task_failure(
|
await task_logger.log_task_failure(
|
||||||
log_entry,
|
log_entry,
|
||||||
|
|
@ -103,6 +107,22 @@ async def index_slack_messages(
|
||||||
)
|
)
|
||||||
return 0, "Slack token not found in connector config"
|
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
|
||||||
await task_logger.log_task_progress(
|
await task_logger.log_task_progress(
|
||||||
log_entry,
|
log_entry,
|
||||||
|
|
@ -112,6 +132,12 @@ async def index_slack_messages(
|
||||||
|
|
||||||
slack_client = SlackHistory(token=slack_token)
|
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
|
# Calculate date range
|
||||||
await task_logger.log_task_progress(
|
await task_logger.log_task_progress(
|
||||||
log_entry,
|
log_entry,
|
||||||
|
|
|
||||||
|
|
@ -513,7 +513,22 @@ def validate_connector_config(
|
||||||
],
|
],
|
||||||
"validators": {},
|
"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": {
|
"GITHUB_CONNECTOR": {
|
||||||
"required": ["GITHUB_PAT", "repo_full_names"],
|
"required": ["GITHUB_PAT", "repo_full_names"],
|
||||||
"validators": {
|
"validators": {
|
||||||
|
|
|
||||||
|
|
@ -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<typeof slackConnectorFormSchema>;
|
|
||||||
|
|
||||||
export const SlackConnectForm: FC<ConnectFormProps> = ({ onSubmit, isSubmitting }) => {
|
|
||||||
const isSubmittingRef = useRef(false);
|
|
||||||
const [startDate, setStartDate] = useState<Date | undefined>(undefined);
|
|
||||||
const [endDate, setEndDate] = useState<Date | undefined>(undefined);
|
|
||||||
const [periodicEnabled, setPeriodicEnabled] = useState(false);
|
|
||||||
const [frequencyMinutes, setFrequencyMinutes] = useState("1440");
|
|
||||||
const form = useForm<SlackConnectorFormValues>({
|
|
||||||
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 (
|
|
||||||
<div className="space-y-6 pb-6">
|
|
||||||
<Alert className="bg-slate-400/5 dark:bg-white/5 border-slate-400/20 p-2 sm:p-3 flex items-center [&>svg]:relative [&>svg]:left-0 [&>svg]:top-0 [&>svg+div]:translate-y-0">
|
|
||||||
<Info className="h-3 w-3 sm:h-4 sm:w-4 shrink-0 ml-1" />
|
|
||||||
<div className="-ml-1">
|
|
||||||
<AlertTitle className="text-xs sm:text-sm">Bot User OAuth Token Required</AlertTitle>
|
|
||||||
<AlertDescription className="text-[10px] sm:text-xs !pl-0">
|
|
||||||
You'll need a Slack Bot User OAuth Token to use this connector. You can create a Slack
|
|
||||||
app and get the token from{" "}
|
|
||||||
<a
|
|
||||||
href="https://api.slack.com/apps"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="font-medium underline underline-offset-4"
|
|
||||||
>
|
|
||||||
Slack API Dashboard
|
|
||||||
</a>
|
|
||||||
</AlertDescription>
|
|
||||||
</div>
|
|
||||||
</Alert>
|
|
||||||
|
|
||||||
<div className="rounded-xl border border-border bg-slate-400/5 dark:bg-white/5 p-3 sm:p-6 space-y-3 sm:space-y-4">
|
|
||||||
<Form {...form}>
|
|
||||||
<form
|
|
||||||
id="slack-connect-form"
|
|
||||||
onSubmit={form.handleSubmit(handleSubmit)}
|
|
||||||
className="space-y-4 sm:space-y-6"
|
|
||||||
>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="name"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel className="text-xs sm:text-sm">Connector Name</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
placeholder="My Slack Connector"
|
|
||||||
className="h-8 sm:h-10 px-2 sm:px-3 text-xs sm:text-sm border-slate-400/20 focus-visible:border-slate-400/40"
|
|
||||||
disabled={isSubmitting}
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription className="text-[10px] sm:text-xs">
|
|
||||||
A friendly name to identify this connector.
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="bot_token"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel className="text-xs sm:text-sm">Slack Bot User OAuth Token</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
type="password"
|
|
||||||
placeholder="xoxb-..."
|
|
||||||
className="h-8 sm:h-10 px-2 sm:px-3 text-xs sm:text-sm border-slate-400/20 focus-visible:border-slate-400/40"
|
|
||||||
disabled={isSubmitting}
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription className="text-[10px] sm:text-xs">
|
|
||||||
Your Bot User OAuth Token will be encrypted and stored securely. It typically
|
|
||||||
starts with "xoxb-".
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Indexing Configuration */}
|
|
||||||
<div className="space-y-4 pt-4 border-t border-slate-400/20">
|
|
||||||
<h3 className="text-sm sm:text-base font-medium">Indexing Configuration</h3>
|
|
||||||
|
|
||||||
{/* Date Range Selector */}
|
|
||||||
<DateRangeSelector
|
|
||||||
startDate={startDate}
|
|
||||||
endDate={endDate}
|
|
||||||
onStartDateChange={setStartDate}
|
|
||||||
onEndDateChange={setEndDate}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Periodic Sync Config */}
|
|
||||||
<div className="rounded-xl bg-slate-400/5 dark:bg-white/5 p-3 sm:p-6">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="space-y-1">
|
|
||||||
<h3 className="font-medium text-sm sm:text-base">Enable Periodic Sync</h3>
|
|
||||||
<p className="text-xs sm:text-sm text-muted-foreground">
|
|
||||||
Automatically re-index at regular intervals
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Switch
|
|
||||||
checked={periodicEnabled}
|
|
||||||
onCheckedChange={setPeriodicEnabled}
|
|
||||||
disabled={isSubmitting}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{periodicEnabled && (
|
|
||||||
<div className="mt-4 pt-4 border-t border-slate-400/20 space-y-3">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="frequency" className="text-xs sm:text-sm">
|
|
||||||
Sync Frequency
|
|
||||||
</Label>
|
|
||||||
<Select
|
|
||||||
value={frequencyMinutes}
|
|
||||||
onValueChange={setFrequencyMinutes}
|
|
||||||
disabled={isSubmitting}
|
|
||||||
>
|
|
||||||
<SelectTrigger
|
|
||||||
id="frequency"
|
|
||||||
className="w-full bg-slate-400/5 dark:bg-slate-400/5 border-slate-400/20 text-xs sm:text-sm"
|
|
||||||
>
|
|
||||||
<SelectValue placeholder="Select frequency" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent className="z-[100]">
|
|
||||||
<SelectItem value="5" className="text-xs sm:text-sm">
|
|
||||||
Every 5 minutes
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="15" className="text-xs sm:text-sm">
|
|
||||||
Every 15 minutes
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="60" className="text-xs sm:text-sm">
|
|
||||||
Every hour
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="360" className="text-xs sm:text-sm">
|
|
||||||
Every 6 hours
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="720" className="text-xs sm:text-sm">
|
|
||||||
Every 12 hours
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="1440" className="text-xs sm:text-sm">
|
|
||||||
Daily
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="10080" className="text-xs sm:text-sm">
|
|
||||||
Weekly
|
|
||||||
</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* What you get section */}
|
|
||||||
{getConnectorBenefits(EnumConnectorName.SLACK_CONNECTOR) && (
|
|
||||||
<div className="rounded-xl border border-border bg-slate-400/5 dark:bg-white/5 px-3 sm:px-6 py-4 space-y-2">
|
|
||||||
<h4 className="text-xs sm:text-sm font-medium">What you get with Slack integration:</h4>
|
|
||||||
<ul className="list-disc pl-5 text-[10px] sm:text-xs text-muted-foreground space-y-1">
|
|
||||||
{getConnectorBenefits(EnumConnectorName.SLACK_CONNECTOR)?.map((benefit) => (
|
|
||||||
<li key={benefit}>{benefit}</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Documentation Section */}
|
|
||||||
<Accordion
|
|
||||||
type="single"
|
|
||||||
collapsible
|
|
||||||
className="w-full border border-border rounded-xl bg-slate-400/5 dark:bg-white/5"
|
|
||||||
>
|
|
||||||
<AccordionItem value="documentation" className="border-0">
|
|
||||||
<AccordionTrigger className="text-sm sm:text-base font-medium px-3 sm:px-6 no-underline hover:no-underline">
|
|
||||||
Documentation
|
|
||||||
</AccordionTrigger>
|
|
||||||
<AccordionContent className="px-3 sm:px-6 pb-3 sm:pb-6 space-y-6">
|
|
||||||
<div>
|
|
||||||
<h3 className="text-sm sm:text-base font-semibold mb-2">How it works</h3>
|
|
||||||
<p className="text-[10px] sm:text-xs text-muted-foreground">
|
|
||||||
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.
|
|
||||||
</p>
|
|
||||||
<ul className="mt-2 list-disc pl-5 text-[10px] sm:text-xs text-muted-foreground space-y-1">
|
|
||||||
<li>
|
|
||||||
For follow up indexing runs, the connector retrieves messages that have been
|
|
||||||
updated since the last indexing attempt.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Indexing is configured to run periodically, so updates should appear in your
|
|
||||||
search results within minutes.
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<h3 className="text-sm sm:text-base font-semibold mb-2">Authorization</h3>
|
|
||||||
<Alert className="bg-slate-400/5 dark:bg-white/5 border-slate-400/20 mb-4">
|
|
||||||
<Info className="h-3 w-3 sm:h-4 sm:w-4" />
|
|
||||||
<AlertTitle className="text-[10px] sm:text-xs">
|
|
||||||
Bot User OAuth Token Required
|
|
||||||
</AlertTitle>
|
|
||||||
<AlertDescription className="text-[9px] sm:text-[10px]">
|
|
||||||
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.
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
|
|
||||||
<div className="space-y-4 sm:space-y-6">
|
|
||||||
<div>
|
|
||||||
<h4 className="text-[10px] sm:text-xs font-medium mb-2">
|
|
||||||
Step 1: Create a Slack App
|
|
||||||
</h4>
|
|
||||||
<ol className="list-decimal pl-5 space-y-2 text-[10px] sm:text-xs text-muted-foreground">
|
|
||||||
<li>
|
|
||||||
Go to{" "}
|
|
||||||
<a
|
|
||||||
href="https://api.slack.com/apps"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="font-medium underline underline-offset-4"
|
|
||||||
>
|
|
||||||
https://api.slack.com/apps
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Click <strong>Create New App</strong> and choose "From scratch"
|
|
||||||
</li>
|
|
||||||
<li>Enter an app name and select your workspace</li>
|
|
||||||
<li>
|
|
||||||
Click <strong>Create App</strong>
|
|
||||||
</li>
|
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h4 className="text-[10px] sm:text-xs font-medium mb-2">
|
|
||||||
Step 2: Configure Bot Scopes
|
|
||||||
</h4>
|
|
||||||
<ol className="list-decimal pl-5 space-y-2 text-[10px] sm:text-xs text-muted-foreground">
|
|
||||||
<li>
|
|
||||||
Navigate to <strong>OAuth & Permissions</strong> in the sidebar
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Under <strong>Bot Token Scopes</strong>, add the following scopes:
|
|
||||||
<ul className="list-disc pl-5 mt-1 space-y-1">
|
|
||||||
<li>
|
|
||||||
<code className="bg-muted px-1 py-0.5 rounded">channels:read</code> -
|
|
||||||
View basic information about public channels
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<code className="bg-muted px-1 py-0.5 rounded">channels:history</code> -
|
|
||||||
View messages in public channels
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<code className="bg-muted px-1 py-0.5 rounded">groups:read</code> - View
|
|
||||||
basic information about private channels
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<code className="bg-muted px-1 py-0.5 rounded">groups:history</code> -
|
|
||||||
View messages in private channels
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<code className="bg-muted px-1 py-0.5 rounded">im:read</code> - View
|
|
||||||
basic information about direct messages
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<code className="bg-muted px-1 py-0.5 rounded">im:history</code> - View
|
|
||||||
messages in direct messages
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h4 className="text-[10px] sm:text-xs font-medium mb-2">
|
|
||||||
Step 3: Install App to Workspace
|
|
||||||
</h4>
|
|
||||||
<ol className="list-decimal pl-5 space-y-2 text-[10px] sm:text-xs text-muted-foreground">
|
|
||||||
<li>
|
|
||||||
Go to <strong>Install App</strong> in the sidebar
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Click <strong>Install to Workspace</strong>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Review the permissions and click <strong>Allow</strong>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Copy the <strong>Bot User OAuth Token</strong> from the "OAuth &
|
|
||||||
Permissions" page (starts with "xoxb-")
|
|
||||||
</li>
|
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<h3 className="text-sm sm:text-base font-semibold mb-2">Indexing</h3>
|
|
||||||
<ol className="list-decimal pl-5 space-y-2 text-[10px] sm:text-xs text-muted-foreground mb-4">
|
|
||||||
<li>
|
|
||||||
Navigate to the Connector Dashboard and select the <strong>Slack</strong>{" "}
|
|
||||||
Connector.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Place the <strong>Bot User OAuth Token</strong> in the form field.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Click <strong>Connect</strong> to establish the connection.
|
|
||||||
</li>
|
|
||||||
<li>Once connected, your Slack messages will be indexed automatically.</li>
|
|
||||||
</ol>
|
|
||||||
|
|
||||||
<Alert className="bg-slate-400/5 dark:bg-white/5 border-slate-400/20">
|
|
||||||
<Info className="h-3 w-3 sm:h-4 sm:w-4" />
|
|
||||||
<AlertTitle className="text-[10px] sm:text-xs">What Gets Indexed</AlertTitle>
|
|
||||||
<AlertDescription className="text-[9px] sm:text-[10px]">
|
|
||||||
<p className="mb-2">The Slack connector indexes the following data:</p>
|
|
||||||
<ul className="list-disc pl-5 space-y-1">
|
|
||||||
<li>Messages from all accessible channels (public and private)</li>
|
|
||||||
<li>Direct messages (if bot has access)</li>
|
|
||||||
<li>Message timestamps and metadata</li>
|
|
||||||
<li>Thread replies and conversations</li>
|
|
||||||
</ul>
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</AccordionContent>
|
|
||||||
</AccordionItem>
|
|
||||||
</Accordion>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -11,7 +11,6 @@ import { JiraConnectForm } from "./components/jira-connect-form";
|
||||||
import { LinkupApiConnectForm } from "./components/linkup-api-connect-form";
|
import { LinkupApiConnectForm } from "./components/linkup-api-connect-form";
|
||||||
import { LumaConnectForm } from "./components/luma-connect-form";
|
import { LumaConnectForm } from "./components/luma-connect-form";
|
||||||
import { SearxngConnectForm } from "./components/searxng-connect-form";
|
import { SearxngConnectForm } from "./components/searxng-connect-form";
|
||||||
import { SlackConnectForm } from "./components/slack-connect-form";
|
|
||||||
import { TavilyApiConnectForm } from "./components/tavily-api-connect-form";
|
import { TavilyApiConnectForm } from "./components/tavily-api-connect-form";
|
||||||
|
|
||||||
export interface ConnectFormProps {
|
export interface ConnectFormProps {
|
||||||
|
|
@ -51,8 +50,6 @@ export function getConnectFormComponent(connectorType: string): ConnectFormCompo
|
||||||
return BaiduSearchApiConnectForm;
|
return BaiduSearchApiConnectForm;
|
||||||
case "ELASTICSEARCH_CONNECTOR":
|
case "ELASTICSEARCH_CONNECTOR":
|
||||||
return ElasticsearchConnectForm;
|
return ElasticsearchConnectForm;
|
||||||
case "SLACK_CONNECTOR":
|
|
||||||
return SlackConnectForm;
|
|
||||||
case "DISCORD_CONNECTOR":
|
case "DISCORD_CONNECTOR":
|
||||||
return DiscordConnectForm;
|
return DiscordConnectForm;
|
||||||
case "CONFLUENCE_CONNECTOR":
|
case "CONFLUENCE_CONNECTOR":
|
||||||
|
|
|
||||||
|
|
@ -1,84 +1,27 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { KeyRound } from "lucide-react";
|
import { Info } from "lucide-react";
|
||||||
import type { FC } from "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";
|
import type { ConnectorConfigProps } from "../index";
|
||||||
|
|
||||||
export interface SlackConfigProps extends ConnectorConfigProps {
|
export interface SlackConfigProps extends ConnectorConfigProps {
|
||||||
onNameChange?: (name: string) => void;
|
onNameChange?: (name: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SlackConfig: FC<SlackConfigProps> = ({ connector, onConfigChange, onNameChange }) => {
|
export const SlackConfig: FC<SlackConfigProps> = () => {
|
||||||
const [botToken, setBotToken] = useState<string>(
|
|
||||||
(connector.config?.SLACK_BOT_TOKEN as string) || ""
|
|
||||||
);
|
|
||||||
const [name, setName] = useState<string>(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);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Connector Name */}
|
<div className="rounded-xl border border-border bg-primary/5 p-4 flex items-start gap-3">
|
||||||
<div className="rounded-xl border border-border bg-slate-400/5 dark:bg-white/5 p-3 sm:p-6 space-y-3 sm:space-y-4">
|
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-primary/10 shrink-0 mt-0.5">
|
||||||
<div className="space-y-2">
|
<Info className="size-4" />
|
||||||
<Label className="text-xs sm:text-sm">Connector Name</Label>
|
|
||||||
<Input
|
|
||||||
value={name}
|
|
||||||
onChange={(e) => handleNameChange(e.target.value)}
|
|
||||||
placeholder="My Slack Connector"
|
|
||||||
className="border-slate-400/20 focus-visible:border-slate-400/40"
|
|
||||||
/>
|
|
||||||
<p className="text-[10px] sm:text-xs text-muted-foreground">
|
|
||||||
A friendly name to identify this connector.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="text-xs sm:text-sm">
|
||||||
|
<p className="font-medium text-xs sm:text-sm">Add Bot to Channels</p>
|
||||||
{/* Configuration */}
|
<p className="text-muted-foreground mt-1 text-[10px] sm:text-sm">
|
||||||
<div className="rounded-xl border border-border bg-slate-400/5 dark:bg-white/5 p-3 sm:p-6 space-y-3 sm:space-y-4">
|
Before indexing, add the SurfSense bot to each channel you want to index. The bot can
|
||||||
<div className="space-y-1 sm:space-y-2">
|
only access messages from channels it's been added to. Type{" "}
|
||||||
<h3 className="font-medium text-sm sm:text-base">Configuration</h3>
|
<code className="bg-muted px-1 py-0.5 rounded text-[9px]">/invite @SurfSense</code> in
|
||||||
</div>
|
any channel to add it.
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label className="flex items-center gap-2 text-xs sm:text-sm">
|
|
||||||
<KeyRound className="h-4 w-4" />
|
|
||||||
Slack Bot User OAuth Token
|
|
||||||
</Label>
|
|
||||||
<Input
|
|
||||||
type="password"
|
|
||||||
value={botToken}
|
|
||||||
onChange={(e) => handleBotTokenChange(e.target.value)}
|
|
||||||
placeholder="Begins with xoxb-..."
|
|
||||||
className="border-slate-400/20 focus-visible:border-slate-400/40"
|
|
||||||
/>
|
|
||||||
<p className="text-[10px] sm:text-xs text-muted-foreground">
|
|
||||||
Update your Bot User OAuth Token if needed.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,6 @@ export const ConnectorConnectView: FC<ConnectorConnectViewProps> = ({
|
||||||
LINKUP_API: "linkup-api-connect-form",
|
LINKUP_API: "linkup-api-connect-form",
|
||||||
BAIDU_SEARCH_API: "baidu-search-api-connect-form",
|
BAIDU_SEARCH_API: "baidu-search-api-connect-form",
|
||||||
ELASTICSEARCH_CONNECTOR: "elasticsearch-connect-form",
|
ELASTICSEARCH_CONNECTOR: "elasticsearch-connect-form",
|
||||||
SLACK_CONNECTOR: "slack-connect-form",
|
|
||||||
DISCORD_CONNECTOR: "discord-connect-form",
|
DISCORD_CONNECTOR: "discord-connect-form",
|
||||||
CONFLUENCE_CONNECTOR: "confluence-connect-form",
|
CONFLUENCE_CONNECTOR: "confluence-connect-form",
|
||||||
BOOKSTACK_CONNECTOR: "bookstack-connect-form",
|
BOOKSTACK_CONNECTOR: "bookstack-connect-form",
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,13 @@ export const OAUTH_CONNECTORS = [
|
||||||
connectorType: EnumConnectorName.LINEAR_CONNECTOR,
|
connectorType: EnumConnectorName.LINEAR_CONNECTOR,
|
||||||
authEndpoint: "/api/v1/auth/linear/connector/add/",
|
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;
|
] as const;
|
||||||
|
|
||||||
// Content Sources (tools that extract and import content from external sources)
|
// 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)
|
// Non-OAuth Connectors (redirect to old connector config pages)
|
||||||
export const OTHER_CONNECTORS = [
|
export const OTHER_CONNECTORS = [
|
||||||
{
|
|
||||||
id: "slack-connector",
|
|
||||||
title: "Slack",
|
|
||||||
description: "Search Slack messages",
|
|
||||||
connectorType: EnumConnectorName.SLACK_CONNECTOR,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: "discord-connector",
|
id: "discord-connector",
|
||||||
title: "Discord",
|
title: "Discord",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue