diff --git a/surfsense_backend/app/agents/new_chat/tools/gmail/create_draft.py b/surfsense_backend/app/agents/new_chat/tools/gmail/create_draft.py index 246c7d16f..aed5669fb 100644 --- a/surfsense_backend/app/agents/new_chat/tools/gmail/create_draft.py +++ b/surfsense_backend/app/agents/new_chat/tools/gmail/create_draft.py @@ -263,10 +263,29 @@ def create_create_gmail_draft_tool( logger.warning( f"Insufficient permissions for connector {actual_connector_id}: {api_err}" ) + try: + from sqlalchemy.orm.attributes import flag_modified + + _res = await db_session.execute( + select(SearchSourceConnector).where( + SearchSourceConnector.id == actual_connector_id + ) + ) + _conn = _res.scalar_one_or_none() + if _conn and not _conn.config.get("auth_expired"): + _conn.config = {**_conn.config, "auth_expired": True} + flag_modified(_conn, "config") + await db_session.commit() + except Exception: + logger.warning( + "Failed to persist auth_expired for connector %s", + actual_connector_id, + exc_info=True, + ) return { "status": "insufficient_permissions", "connector_id": actual_connector_id, - "message": "This Gmail account needs additional permissions. Please re-authenticate.", + "message": "This Gmail account needs additional permissions. Please re-authenticate in connector settings.", } raise diff --git a/surfsense_backend/app/agents/new_chat/tools/gmail/send_email.py b/surfsense_backend/app/agents/new_chat/tools/gmail/send_email.py index 93c7933a5..dd4d66a57 100644 --- a/surfsense_backend/app/agents/new_chat/tools/gmail/send_email.py +++ b/surfsense_backend/app/agents/new_chat/tools/gmail/send_email.py @@ -264,10 +264,29 @@ def create_send_gmail_email_tool( logger.warning( f"Insufficient permissions for connector {actual_connector_id}: {api_err}" ) + try: + from sqlalchemy.orm.attributes import flag_modified + + _res = await db_session.execute( + select(SearchSourceConnector).where( + SearchSourceConnector.id == actual_connector_id + ) + ) + _conn = _res.scalar_one_or_none() + if _conn and not _conn.config.get("auth_expired"): + _conn.config = {**_conn.config, "auth_expired": True} + flag_modified(_conn, "config") + await db_session.commit() + except Exception: + logger.warning( + "Failed to persist auth_expired for connector %s", + actual_connector_id, + exc_info=True, + ) return { "status": "insufficient_permissions", "connector_id": actual_connector_id, - "message": "This Gmail account needs additional permissions. Please re-authenticate.", + "message": "This Gmail account needs additional permissions. Please re-authenticate in connector settings.", } raise diff --git a/surfsense_backend/app/agents/new_chat/tools/gmail/trash_email.py b/surfsense_backend/app/agents/new_chat/tools/gmail/trash_email.py index 417839bb1..9e8edb47e 100644 --- a/surfsense_backend/app/agents/new_chat/tools/gmail/trash_email.py +++ b/surfsense_backend/app/agents/new_chat/tools/gmail/trash_email.py @@ -254,10 +254,23 @@ def create_trash_gmail_email_tool( logger.warning( f"Insufficient permissions for connector {connector.id}: {api_err}" ) + try: + from sqlalchemy.orm.attributes import flag_modified + + if not connector.config.get("auth_expired"): + connector.config = {**connector.config, "auth_expired": True} + flag_modified(connector, "config") + await db_session.commit() + except Exception: + logger.warning( + "Failed to persist auth_expired for connector %s", + connector.id, + exc_info=True, + ) return { "status": "insufficient_permissions", "connector_id": connector.id, - "message": "This Gmail account needs additional permissions. Please re-authenticate.", + "message": "This Gmail account needs additional permissions. Please re-authenticate in connector settings.", } raise diff --git a/surfsense_backend/app/agents/new_chat/tools/google_calendar/create_event.py b/surfsense_backend/app/agents/new_chat/tools/google_calendar/create_event.py index cf96870df..14a43975c 100644 --- a/surfsense_backend/app/agents/new_chat/tools/google_calendar/create_event.py +++ b/surfsense_backend/app/agents/new_chat/tools/google_calendar/create_event.py @@ -251,12 +251,45 @@ def create_create_calendar_event_tool( {"email": e.strip()} for e in final_attendees if e.strip() ] - created = await asyncio.get_event_loop().run_in_executor( - None, - lambda: service.events() - .insert(calendarId="primary", body=event_body) - .execute(), - ) + try: + created = await asyncio.get_event_loop().run_in_executor( + None, + lambda: service.events() + .insert(calendarId="primary", body=event_body) + .execute(), + ) + except Exception as api_err: + from googleapiclient.errors import HttpError + + if isinstance(api_err, HttpError) and api_err.resp.status == 403: + logger.warning( + f"Insufficient permissions for connector {actual_connector_id}: {api_err}" + ) + try: + from sqlalchemy.orm.attributes import flag_modified + + _res = await db_session.execute( + select(SearchSourceConnector).where( + SearchSourceConnector.id == actual_connector_id + ) + ) + _conn = _res.scalar_one_or_none() + if _conn and not _conn.config.get("auth_expired"): + _conn.config = {**_conn.config, "auth_expired": True} + flag_modified(_conn, "config") + await db_session.commit() + except Exception: + logger.warning( + "Failed to persist auth_expired for connector %s", + actual_connector_id, + exc_info=True, + ) + return { + "status": "insufficient_permissions", + "connector_id": actual_connector_id, + "message": "This Google Calendar account needs additional permissions. Please re-authenticate in connector settings.", + } + raise logger.info( f"Calendar event created: id={created.get('id')}, summary={created.get('summary')}" diff --git a/surfsense_backend/app/agents/new_chat/tools/google_calendar/delete_event.py b/surfsense_backend/app/agents/new_chat/tools/google_calendar/delete_event.py index 3a4570737..04858751f 100644 --- a/surfsense_backend/app/agents/new_chat/tools/google_calendar/delete_event.py +++ b/surfsense_backend/app/agents/new_chat/tools/google_calendar/delete_event.py @@ -230,12 +230,45 @@ def create_delete_calendar_event_tool( None, lambda: build("calendar", "v3", credentials=creds) ) - await asyncio.get_event_loop().run_in_executor( - None, - lambda: service.events() - .delete(calendarId="primary", eventId=final_event_id) - .execute(), - ) + try: + await asyncio.get_event_loop().run_in_executor( + None, + lambda: service.events() + .delete(calendarId="primary", eventId=final_event_id) + .execute(), + ) + except Exception as api_err: + from googleapiclient.errors import HttpError + + if isinstance(api_err, HttpError) and api_err.resp.status == 403: + logger.warning( + f"Insufficient permissions for connector {actual_connector_id}: {api_err}" + ) + try: + from sqlalchemy.orm.attributes import flag_modified + + _res = await db_session.execute( + select(SearchSourceConnector).where( + SearchSourceConnector.id == actual_connector_id + ) + ) + _conn = _res.scalar_one_or_none() + if _conn and not _conn.config.get("auth_expired"): + _conn.config = {**_conn.config, "auth_expired": True} + flag_modified(_conn, "config") + await db_session.commit() + except Exception: + logger.warning( + "Failed to persist auth_expired for connector %s", + actual_connector_id, + exc_info=True, + ) + return { + "status": "insufficient_permissions", + "connector_id": actual_connector_id, + "message": "This Google Calendar account needs additional permissions. Please re-authenticate in connector settings.", + } + raise logger.info( f"Calendar event deleted: event_id={final_event_id}" diff --git a/surfsense_backend/app/agents/new_chat/tools/google_calendar/update_event.py b/surfsense_backend/app/agents/new_chat/tools/google_calendar/update_event.py index 98f20a3c7..505b07f39 100644 --- a/surfsense_backend/app/agents/new_chat/tools/google_calendar/update_event.py +++ b/surfsense_backend/app/agents/new_chat/tools/google_calendar/update_event.py @@ -271,12 +271,45 @@ def create_update_calendar_event_tool( "message": "No changes specified. Please provide at least one field to update.", } - updated = await asyncio.get_event_loop().run_in_executor( - None, - lambda: service.events() - .patch(calendarId="primary", eventId=final_event_id, body=update_body) - .execute(), - ) + try: + updated = await asyncio.get_event_loop().run_in_executor( + None, + lambda: service.events() + .patch(calendarId="primary", eventId=final_event_id, body=update_body) + .execute(), + ) + except Exception as api_err: + from googleapiclient.errors import HttpError + + if isinstance(api_err, HttpError) and api_err.resp.status == 403: + logger.warning( + f"Insufficient permissions for connector {actual_connector_id}: {api_err}" + ) + try: + from sqlalchemy.orm.attributes import flag_modified + + _res = await db_session.execute( + select(SearchSourceConnector).where( + SearchSourceConnector.id == actual_connector_id + ) + ) + _conn = _res.scalar_one_or_none() + if _conn and not _conn.config.get("auth_expired"): + _conn.config = {**_conn.config, "auth_expired": True} + flag_modified(_conn, "config") + await db_session.commit() + except Exception: + logger.warning( + "Failed to persist auth_expired for connector %s", + actual_connector_id, + exc_info=True, + ) + return { + "status": "insufficient_permissions", + "connector_id": actual_connector_id, + "message": "This Google Calendar account needs additional permissions. Please re-authenticate in connector settings.", + } + raise logger.info( f"Calendar event updated: event_id={final_event_id}" diff --git a/surfsense_backend/app/agents/new_chat/tools/google_drive/create_file.py b/surfsense_backend/app/agents/new_chat/tools/google_drive/create_file.py index 7a990c98d..d39e9c640 100644 --- a/surfsense_backend/app/agents/new_chat/tools/google_drive/create_file.py +++ b/surfsense_backend/app/agents/new_chat/tools/google_drive/create_file.py @@ -232,10 +232,29 @@ def create_create_google_drive_file_tool( logger.warning( f"Insufficient permissions for connector {actual_connector_id}: {http_err}" ) + try: + from sqlalchemy.orm.attributes import flag_modified + + _res = await db_session.execute( + select(SearchSourceConnector).where( + SearchSourceConnector.id == actual_connector_id + ) + ) + _conn = _res.scalar_one_or_none() + if _conn and not _conn.config.get("auth_expired"): + _conn.config = {**_conn.config, "auth_expired": True} + flag_modified(_conn, "config") + await db_session.commit() + except Exception: + logger.warning( + "Failed to persist auth_expired for connector %s", + actual_connector_id, + exc_info=True, + ) return { "status": "insufficient_permissions", "connector_id": actual_connector_id, - "message": "This Google Drive account needs additional permissions. Please re-authenticate.", + "message": "This Google Drive account needs additional permissions. Please re-authenticate in connector settings.", } raise diff --git a/surfsense_backend/app/agents/new_chat/tools/google_drive/trash_file.py b/surfsense_backend/app/agents/new_chat/tools/google_drive/trash_file.py index 3fcc2532a..4c037625f 100644 --- a/surfsense_backend/app/agents/new_chat/tools/google_drive/trash_file.py +++ b/surfsense_backend/app/agents/new_chat/tools/google_drive/trash_file.py @@ -207,10 +207,23 @@ def create_delete_google_drive_file_tool( logger.warning( f"Insufficient permissions for connector {connector.id}: {http_err}" ) + try: + from sqlalchemy.orm.attributes import flag_modified + + if not connector.config.get("auth_expired"): + connector.config = {**connector.config, "auth_expired": True} + flag_modified(connector, "config") + await db_session.commit() + except Exception: + logger.warning( + "Failed to persist auth_expired for connector %s", + connector.id, + exc_info=True, + ) return { "status": "insufficient_permissions", "connector_id": connector.id, - "message": "This Google Drive account needs additional permissions. Please re-authenticate.", + "message": "This Google Drive account needs additional permissions. Please re-authenticate in connector settings.", } raise 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 3f3201043..9a2308bec 100644 --- a/surfsense_backend/app/routes/google_calendar_add_connector_route.py +++ b/surfsense_backend/app/routes/google_calendar_add_connector_route.py @@ -34,7 +34,7 @@ logger = logging.getLogger(__name__) router = APIRouter() -SCOPES = ["https://www.googleapis.com/auth/calendar.readonly"] +SCOPES = ["https://www.googleapis.com/auth/calendar.events"] REDIRECT_URI = config.GOOGLE_CALENDAR_REDIRECT_URI # Initialize security utilities 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 dd8e6bfac..750a64819 100644 --- a/surfsense_backend/app/routes/google_gmail_add_connector_route.py +++ b/surfsense_backend/app/routes/google_gmail_add_connector_route.py @@ -73,7 +73,7 @@ def get_google_flow(): } }, scopes=[ - "https://www.googleapis.com/auth/gmail.readonly", + "https://www.googleapis.com/auth/gmail.modify", "https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/userinfo.profile", "openid", diff --git a/surfsense_web/components/tool-ui/gmail/create-draft.tsx b/surfsense_web/components/tool-ui/gmail/create-draft.tsx index c42901a4e..4f1181039 100644 --- a/surfsense_web/components/tool-ui/gmail/create-draft.tsx +++ b/surfsense_web/components/tool-ui/gmail/create-draft.tsx @@ -3,8 +3,6 @@ import { makeAssistantToolUI } from "@assistant-ui/react"; import { CornerDownLeftIcon, - FileEditIcon, - MailIcon, Pen, UserIcon, UsersIcon, @@ -66,10 +64,17 @@ interface AuthErrorResult { connector_type?: string; } +interface InsufficientPermissionsResult { + status: "insufficient_permissions"; + connector_id: number; + message: string; +} + type CreateGmailDraftResult = | InterruptResult | SuccessResult | ErrorResult + | InsufficientPermissionsResult | AuthErrorResult; function isInterruptResult(result: unknown): result is InterruptResult { @@ -99,6 +104,15 @@ function isAuthErrorResult(result: unknown): result is AuthErrorResult { ); } +function isInsufficientPermissionsResult(result: unknown): result is InsufficientPermissionsResult { + return ( + typeof result === "object" && + result !== null && + "status" in result && + (result as InsufficientPermissionsResult).status === "insufficient_permissions" + ); +} + function ApprovalCard({ args, interruptData, @@ -168,7 +182,6 @@ function ApprovalCard({ {/* Header */}
{decided === "reject" @@ -388,12 +401,27 @@ function AuthErrorCard({ result }: { result: AuthErrorResult }) { ); } +function InsufficientPermissionsCard({ result }: { result: InsufficientPermissionsResult }) { + return ( +
+ Additional Gmail permissions required +
+{result.message}
+{result.message || "Gmail draft created successfully"}
@@ -443,6 +471,8 @@ export const CreateGmailDraftToolUI = makeAssistantToolUI< } if (isAuthErrorResult(result)) return+ Additional Gmail permissions required +
+{result.message}
++ Additional Gmail permissions required +
+{result.message}
++ Additional Google Calendar permissions required +
+{result.message}
++ Additional Google Calendar permissions required +
+{result.message}
++ Additional Google Calendar permissions required +
+{result.message}
+- Additional permissions required -
-+ Additional Google Drive permissions required +
{result.message}
-- Additional permissions required -
-+ Additional Google Drive permissions required +
{result.message}
-