mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-01-19 10:50:11 -06:00
fix: Complete integration implementations and improve error handling
HIGH PRIORITY FIXES: - GitHub: Fix webhook signature verification to use raw request body bytes * GitHub signs the raw body, not parsed JSON, so signature verification was failing * Added security checks to reject webhooks when secret configured but signature missing * Updated route handler to pass raw body for proper signature verification - QuickBooks: Complete customer/account mapping implementation * Implemented automatic customer lookup by DisplayName in QuickBooks * Implemented automatic item lookup by Name for invoice items * Implemented automatic expense account discovery with fallback * Auto-saves discovered mappings for future use * Added validation to ensure required mappings exist before creating invoices * Improved API request handling with proper error messages and timeout handling IMPROVEMENTS: - CalDAV: Enhance bidirectional sync functionality * Improved update handling to use existing event hrefs correctly * Better error handling for HTTP errors (404, etc.) * Enhanced event creation vs update logic - Integrations: Add comprehensive error handling across all integrations * GitHub: Network errors, authentication failures, database errors, timeout handling * QuickBooks: API errors, validation errors, timeout handling, connection errors * CalDAV: HTTP errors, connection errors, timeout handling * Jira & Slack: Improved error handling in webhook handlers * All integrations now properly handle timeouts, connection errors, auth failures * Detailed error messages and appropriate logging levels * Proper database transaction rollback on errors TECHNICAL CHANGES: - Updated BaseConnector.handle_webhook() to accept optional raw_body parameter - Updated all webhook handlers (GitHub, Jira, Slack) for consistency - Improved QuickBooks API request method with better error handling - Enhanced CalDAV client create_or_update_event() to handle existing hrefs
This commit is contained in:
@@ -120,13 +120,14 @@ class BaseConnector(ABC):
|
||||
# Default implementation - override in subclasses
|
||||
return {"success": False, "message": "Sync not implemented for this connector"}
|
||||
|
||||
def handle_webhook(self, payload: Dict[str, Any], headers: Dict[str, str]) -> Dict[str, Any]:
|
||||
def handle_webhook(self, payload: Dict[str, Any], headers: Dict[str, str], raw_body: Optional[bytes] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Handle incoming webhook from the service.
|
||||
|
||||
Args:
|
||||
payload: Webhook payload
|
||||
payload: Webhook payload (parsed JSON/dict)
|
||||
headers: Request headers
|
||||
raw_body: Raw request body bytes (for signature verification)
|
||||
|
||||
Returns:
|
||||
Dict with processing results
|
||||
|
||||
@@ -261,8 +261,12 @@ class JiraConnector(BaseConnector):
|
||||
}
|
||||
return status_map.get(jira_status, "todo")
|
||||
|
||||
def handle_webhook(self, payload: Dict[str, Any], headers: Dict[str, str]) -> Dict[str, Any]:
|
||||
def handle_webhook(self, payload: Dict[str, Any], headers: Dict[str, str], raw_body: Optional[bytes] = None) -> Dict[str, Any]:
|
||||
"""Handle incoming webhook from Jira."""
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
event_type = payload.get("webhookEvent")
|
||||
issue = payload.get("issue", {})
|
||||
@@ -278,7 +282,11 @@ class JiraConnector(BaseConnector):
|
||||
return {"success": True, "message": f"Webhook received for issue {issue_key}", "event_type": event_type}
|
||||
|
||||
return {"success": True, "message": f"Webhook processed: {event_type}"}
|
||||
except KeyError as e:
|
||||
logger.error(f"Jira webhook missing required field: {e}")
|
||||
return {"success": False, "message": f"Invalid webhook payload: missing field {str(e)}"}
|
||||
except Exception as e:
|
||||
logger.error(f"Jira webhook processing error: {e}", exc_info=True)
|
||||
return {"success": False, "message": f"Error processing webhook: {str(e)}"}
|
||||
|
||||
def get_config_schema(self) -> Dict[str, Any]:
|
||||
|
||||
@@ -217,12 +217,19 @@ class SlackConnector(BaseConnector):
|
||||
except Exception as e:
|
||||
return {"success": False, "message": f"Sync failed: {str(e)}"}
|
||||
|
||||
def handle_webhook(self, payload: Dict[str, Any], headers: Dict[str, str]) -> Dict[str, Any]:
|
||||
def handle_webhook(self, payload: Dict[str, Any], headers: Dict[str, str], raw_body: Optional[bytes] = None) -> Dict[str, Any]:
|
||||
"""Handle incoming webhook from Slack."""
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
# Slack webhooks typically use challenge-response for URL verification
|
||||
if payload.get("type") == "url_verification":
|
||||
return {"success": True, "challenge": payload.get("challenge")}
|
||||
challenge = payload.get("challenge")
|
||||
if not challenge:
|
||||
return {"success": False, "message": "URL verification challenge missing"}
|
||||
return {"success": True, "challenge": challenge}
|
||||
|
||||
event = payload.get("event", {})
|
||||
event_type = event.get("type", "")
|
||||
@@ -232,7 +239,11 @@ class SlackConnector(BaseConnector):
|
||||
return {"success": True, "message": "Message event received", "event_type": event_type}
|
||||
|
||||
return {"success": True, "message": f"Webhook processed: {event_type}"}
|
||||
except KeyError as e:
|
||||
logger.error(f"Slack webhook missing required field: {e}")
|
||||
return {"success": False, "message": f"Invalid webhook payload: missing field {str(e)}"}
|
||||
except Exception as e:
|
||||
logger.error(f"Slack webhook processing error: {e}", exc_info=True)
|
||||
return {"success": False, "message": f"Error processing webhook: {str(e)}"}
|
||||
|
||||
def send_message(self, channel: str, text: str) -> Dict[str, Any]:
|
||||
|
||||
@@ -488,7 +488,8 @@ def integration_webhook(provider):
|
||||
logger.warning(f"Webhook received for unknown provider: {provider}")
|
||||
return jsonify({"error": "Unknown provider"}), 404
|
||||
|
||||
# Get webhook payload
|
||||
# Get webhook payload - preserve raw body for signature verification (GitHub, etc.)
|
||||
raw_body = request.data # Raw bytes for signature verification
|
||||
payload = request.get_json(silent=True) or request.form.to_dict()
|
||||
headers = dict(request.headers)
|
||||
|
||||
@@ -507,8 +508,8 @@ def integration_webhook(provider):
|
||||
if not connector:
|
||||
continue
|
||||
|
||||
# Handle webhook
|
||||
result = connector.handle_webhook(payload, headers)
|
||||
# Handle webhook - pass raw body for signature verification
|
||||
result = connector.handle_webhook(payload, headers, raw_body=raw_body)
|
||||
results.append(
|
||||
{
|
||||
"integration_id": integration.id,
|
||||
|
||||
Reference in New Issue
Block a user