Files
Tomas Dvorak d27cf14110 first test
2026-02-08 14:14:55 +01:00

477 lines
16 KiB
Go

package handlers
import (
"fmt"
"net/http"
"strconv"
"time"
"github.com/gin-gonic/gin"
"github.com/trackeep/backend/models"
"gorm.io/gorm"
)
// IntegrationHandler handles integration-related requests
type IntegrationHandler struct {
db *gorm.DB
}
// NewIntegrationHandler creates a new integration handler
func NewIntegrationHandler(db *gorm.DB) *IntegrationHandler {
return &IntegrationHandler{db: db}
}
// GetIntegrations returns all integrations for the current user
func (h *IntegrationHandler) GetIntegrations(c *gin.Context) {
userID := c.GetString("userID")
var integrations []models.Integration
if err := h.db.Where("user_id = ?", userID).
Preload("SyncLogs", func(db *gorm.DB) *gorm.DB {
return db.Order("created_at DESC").Limit(10)
}).
Find(&integrations).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch integrations"})
return
}
c.JSON(http.StatusOK, gin.H{"integrations": integrations})
}
// GetIntegration returns a specific integration
func (h *IntegrationHandler) GetIntegration(c *gin.Context) {
userID := c.GetString("userID")
integrationID := c.Param("id")
var integration models.Integration
if err := h.db.Where("id = ? AND user_id = ?", integrationID, userID).
Preload("SyncLogs", func(db *gorm.DB) *gorm.DB {
return db.Order("created_at DESC").Limit(50)
}).
First(&integration).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "Integration not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch integration"})
return
}
c.JSON(http.StatusOK, gin.H{"integration": integration})
}
// CreateIntegration creates a new integration
func (h *IntegrationHandler) CreateIntegration(c *gin.Context) {
userID := c.GetString("userID")
var req struct {
Type models.IntegrationType `json:"type" binding:"required"`
Name string `json:"name" binding:"required"`
Description string `json:"description"`
Config models.IntegrationConfig `json:"config"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
integration := models.Integration{
UserID: userID,
Type: req.Type,
Status: models.StatusPending,
Name: req.Name,
Description: req.Description,
Config: req.Config,
SyncEnabled: true,
SyncInterval: 60, // Default 1 hour
}
if err := h.db.Create(&integration).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create integration"})
return
}
c.JSON(http.StatusCreated, gin.H{"integration": integration})
}
// UpdateIntegration updates an existing integration
func (h *IntegrationHandler) UpdateIntegration(c *gin.Context) {
userID := c.GetString("userID")
integrationID := c.Param("id")
var integration models.Integration
if err := h.db.Where("id = ? AND user_id = ?", integrationID, userID).First(&integration).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "Integration not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch integration"})
return
}
var req struct {
Name *string `json:"name"`
Description *string `json:"description"`
Config *models.IntegrationConfig `json:"config"`
SyncEnabled *bool `json:"syncEnabled"`
SyncInterval *int `json:"syncInterval"`
WebhookURL *string `json:"webhookUrl"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Update fields if provided
if req.Name != nil {
integration.Name = *req.Name
}
if req.Description != nil {
integration.Description = *req.Description
}
if req.Config != nil {
integration.Config = *req.Config
}
if req.SyncEnabled != nil {
integration.SyncEnabled = *req.SyncEnabled
}
if req.SyncInterval != nil {
integration.SyncInterval = *req.SyncInterval
}
if req.WebhookURL != nil {
integration.WebhookURL = *req.WebhookURL
}
if err := h.db.Save(&integration).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update integration"})
return
}
c.JSON(http.StatusOK, gin.H{"integration": integration})
}
// DeleteIntegration deletes an integration
func (h *IntegrationHandler) DeleteIntegration(c *gin.Context) {
userID := c.GetString("userID")
integrationID := c.Param("id")
if err := h.db.Where("id = ? AND user_id = ?", integrationID, userID).Delete(&models.Integration{}).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete integration"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Integration deleted successfully"})
}
// AuthorizeIntegration starts the OAuth flow for an integration
func (h *IntegrationHandler) AuthorizeIntegration(c *gin.Context) {
userID := c.GetString("userID")
integrationID := c.Param("id")
var integration models.Integration
if err := h.db.Where("id = ? AND user_id = ?", integrationID, userID).First(&integration).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "Integration not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch integration"})
return
}
// Generate authorization URL based on integration type
var authURL string
switch integration.Type {
case models.IntegrationSlack:
authURL = h.getSlackAuthURL(integration.ID)
case models.IntegrationDiscord:
authURL = h.getDiscordAuthURL(integration.ID)
case models.IntegrationNotion:
authURL = h.getNotionAuthURL(integration.ID)
case models.IntegrationGoogle:
authURL = h.getGoogleAuthURL(integration.ID)
case models.IntegrationGitHub:
authURL = h.getGitHubAuthURL(integration.ID)
default:
c.JSON(http.StatusBadRequest, gin.H{"error": "OAuth not supported for this integration type"})
return
}
c.JSON(http.StatusOK, gin.H{"authUrl": authURL})
}
// OAuthCallback handles the OAuth callback
func (h *IntegrationHandler) OAuthCallback(c *gin.Context) {
integrationID := c.Query("state")
code := c.Query("code")
if integrationID == "" || code == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Missing required parameters"})
return
}
var integration models.Integration
if err := h.db.Where("id = ?", integrationID).First(&integration).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Integration not found"})
return
}
// Exchange code for tokens based on integration type
var accessToken, refreshToken string
var err error
switch integration.Type {
case models.IntegrationSlack:
accessToken, refreshToken, err = h.exchangeSlackCode(code)
case models.IntegrationDiscord:
accessToken, refreshToken, err = h.exchangeDiscordCode(code)
case models.IntegrationNotion:
accessToken, refreshToken, err = h.exchangeNotionCode(code)
case models.IntegrationGoogle:
accessToken, refreshToken, err = h.exchangeGoogleCode(code)
case models.IntegrationGitHub:
accessToken, refreshToken, err = h.exchangeGitHubCode(code)
default:
c.JSON(http.StatusBadRequest, gin.H{"error": "Unsupported integration type"})
return
}
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to exchange authorization code"})
return
}
// Update integration with tokens
integration.AccessToken = accessToken
integration.RefreshToken = refreshToken
integration.Status = models.StatusActive
if err := h.db.Save(&integration).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update integration"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Integration authorized successfully", "integration": integration})
}
// SyncIntegration manually triggers a sync for an integration
func (h *IntegrationHandler) SyncIntegration(c *gin.Context) {
userID := c.GetString("userID")
integrationID := c.Param("id")
var integration models.Integration
if err := h.db.Where("id = ? AND user_id = ?", integrationID, userID).First(&integration).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "Integration not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch integration"})
return
}
if integration.Status != models.StatusActive {
c.JSON(http.StatusBadRequest, gin.H{"error": "Integration is not active"})
return
}
// Start sync in background
go h.performSync(integration)
c.JSON(http.StatusOK, gin.H{"message": "Sync started"})
}
// GetSyncLogs returns sync logs for an integration
func (h *IntegrationHandler) GetSyncLogs(c *gin.Context) {
userID := c.GetString("userID")
integrationID := c.Param("id")
// Verify integration belongs to user
var integration models.Integration
if err := h.db.Where("id = ? AND user_id = ?", integrationID, userID).First(&integration).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "Integration not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch integration"})
return
}
// Parse pagination parameters
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "20"))
offset := (page - 1) * limit
var logs []models.SyncLog
var total int64
if err := h.db.Where("integration_id = ?", integrationID).
Model(&models.SyncLog{}).
Count(&total).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to count sync logs"})
return
}
if err := h.db.Where("integration_id = ?", integrationID).
Order("created_at DESC").
Limit(limit).
Offset(offset).
Find(&logs).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch sync logs"})
return
}
c.JSON(http.StatusOK, gin.H{
"logs": logs,
"pagination": gin.H{
"page": page,
"limit": limit,
"total": total,
},
})
}
// Helper methods for OAuth URLs (these would contain actual OAuth configuration)
func (h *IntegrationHandler) getSlackAuthURL(integrationID string) string {
return fmt.Sprintf("https://slack.com/oauth/v2/authorize?client_id=SLACK_CLIENT_ID&scope=commands,chat:write,users:read&redirect_uri=%s&state=%s",
"http://localhost:8080/api/integrations/oauth/callback", integrationID)
}
func (h *IntegrationHandler) getDiscordAuthURL(integrationID string) string {
return fmt.Sprintf("https://discord.com/api/oauth2/authorize?client_id=DISCORD_CLIENT_ID&scope=bot&permissions=8&redirect_uri=%s&response_type=code&state=%s",
"http://localhost:8080/api/integrations/oauth/callback", integrationID)
}
func (h *IntegrationHandler) getNotionAuthURL(integrationID string) string {
return fmt.Sprintf("https://api.notion.com/v1/oauth/authorize?client_id=NOTION_CLIENT_ID&redirect_uri=%s&response_type=code&state=%s",
"http://localhost:8080/api/integrations/oauth/callback", integrationID)
}
func (h *IntegrationHandler) getGoogleAuthURL(integrationID string) string {
return fmt.Sprintf("https://accounts.google.com/o/oauth2/v2/auth?client_id=GOOGLE_CLIENT_ID&redirect_uri=%s&response_type=code&scope=https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/calendar&state=%s",
"http://localhost:8080/api/integrations/oauth/callback", integrationID)
}
func (h *IntegrationHandler) getGitHubAuthURL(integrationID string) string {
return fmt.Sprintf("https://github.com/login/oauth/authorize?client_id=GITHUB_CLIENT_ID&redirect_uri=%s&scope=repo&state=%s",
"http://localhost:8080/api/integrations/oauth/callback", integrationID)
}
// Helper methods for token exchange (these would contain actual API calls)
func (h *IntegrationHandler) exchangeSlackCode(code string) (string, string, error) {
// TODO: Implement actual Slack token exchange
return "mock_access_token", "mock_refresh_token", nil
}
func (h *IntegrationHandler) exchangeDiscordCode(code string) (string, string, error) {
// TODO: Implement actual Discord token exchange
return "mock_access_token", "mock_refresh_token", nil
}
func (h *IntegrationHandler) exchangeNotionCode(code string) (string, string, error) {
// TODO: Implement actual Notion token exchange
return "mock_access_token", "", nil // Notion doesn't use refresh tokens
}
func (h *IntegrationHandler) exchangeGoogleCode(code string) (string, string, error) {
// TODO: Implement actual Google token exchange
return "mock_access_token", "mock_refresh_token", nil
}
func (h *IntegrationHandler) exchangeGitHubCode(code string) (string, string, error) {
// TODO: Implement actual GitHub token exchange
return "mock_access_token", "", nil // GitHub tokens don't expire
}
// performSync performs the actual sync operation
func (h *IntegrationHandler) performSync(integration models.Integration) {
startTime := time.Now()
syncLog := models.SyncLog{
IntegrationID: integration.ID,
Type: "manual",
Status: "success",
StartedAt: startTime,
}
// Create initial sync log
h.db.Create(&syncLog)
// Perform sync based on integration type
var itemsProcessed, itemsCreated, itemsUpdated, itemsDeleted, itemsSkipped int
var err error
switch integration.Type {
case models.IntegrationSlack:
itemsProcessed, itemsCreated, itemsUpdated, itemsDeleted, itemsSkipped, err = h.syncSlack(integration)
case models.IntegrationDiscord:
itemsProcessed, itemsCreated, itemsUpdated, itemsDeleted, itemsSkipped, err = h.syncDiscord(integration)
case models.IntegrationNotion:
itemsProcessed, itemsCreated, itemsUpdated, itemsDeleted, itemsSkipped, err = h.syncNotion(integration)
case models.IntegrationGoogle:
itemsProcessed, itemsCreated, itemsUpdated, itemsDeleted, itemsSkipped, err = h.syncGoogle(integration)
case models.IntegrationGitHub:
itemsProcessed, itemsCreated, itemsUpdated, itemsDeleted, itemsSkipped, err = h.syncGitHub(integration)
default:
err = fmt.Errorf("unsupported integration type")
}
// Update sync log
completedAt := time.Now()
duration := int(completedAt.Sub(startTime).Seconds())
if err != nil {
syncLog.Status = "error"
syncLog.ErrorMessage = err.Error()
// Update integration error count
integration.ErrorCount++
integration.LastError = err.Error()
} else {
syncLog.ItemsProcessed = itemsProcessed
syncLog.ItemsCreated = itemsCreated
syncLog.ItemsUpdated = itemsUpdated
syncLog.ItemsDeleted = itemsDeleted
syncLog.ItemsSkipped = itemsSkipped
// Update integration sync count
integration.SyncCount++
integration.LastError = ""
}
syncLog.CompletedAt = &completedAt
syncLog.Duration = duration
h.db.Save(&syncLog)
// Update integration
integration.LastSyncAt = &completedAt
h.db.Save(&integration)
}
// Mock sync methods (these would contain actual API calls)
func (h *IntegrationHandler) syncSlack(integration models.Integration) (int, int, int, int, int, error) {
// TODO: Implement actual Slack sync
return 0, 0, 0, 0, 0, nil
}
func (h *IntegrationHandler) syncDiscord(integration models.Integration) (int, int, int, int, int, error) {
// TODO: Implement actual Discord sync
return 0, 0, 0, 0, 0, nil
}
func (h *IntegrationHandler) syncNotion(integration models.Integration) (int, int, int, int, int, error) {
// TODO: Implement actual Notion sync
return 0, 0, 0, 0, 0, nil
}
func (h *IntegrationHandler) syncGoogle(integration models.Integration) (int, int, int, int, int, error) {
// TODO: Implement actual Google sync
return 0, 0, 0, 0, 0, nil
}
func (h *IntegrationHandler) syncGitHub(integration models.Integration) (int, int, int, int, int, error) {
// TODO: Implement actual GitHub sync
return 0, 0, 0, 0, 0, nil
}