package httpapi import ( "encoding/json" "fmt" "net/http" "net/url" "strings" "github.com/gin-gonic/gin" "github.com/google/uuid" "productier/apps/backend/internal/store" ) // OAuth state for CSRF protection type oauthState struct { State string `json:"state"` Provider string `json:"provider"` WorkspaceSlug string `json:"workspaceSlug"` RedirectURL string `json:"redirectUrl"` } // In-memory state store (in production, use Redis or database) var oauthStates = make(map[string]oauthState) func (s *Server) registerOAuthRoutes(group *gin.RouterGroup) { // Google Calendar OAuth group.GET("/oauth/google-calendar/connect", func(c *gin.Context) { workspaceSlug := c.Query("workspaceSlug") if _, ok := s.requireWorkspaceMember(c, workspaceSlug); !ok { return } state := uuid.NewString() redirectURL := c.Query("redirect") if redirectURL == "" { redirectURL = fmt.Sprintf("/app/%s/integrations", workspaceSlug) } oauthStates[state] = oauthState{ State: state, Provider: "google_calendar", WorkspaceSlug: workspaceSlug, RedirectURL: redirectURL, } // Build Google OAuth URL // In production, use actual OAuth credentials from config authURL := fmt.Sprintf( "https://accounts.google.com/o/oauth2/v2/auth?client_id=%s&redirect_uri=%s&response_type=code&scope=%s&state=%s&access_type=offline&prompt=consent", url.QueryEscape(s.config.GoogleClientID), url.QueryEscape(s.config.GoogleRedirectURI), url.QueryEscape("https://www.googleapis.com/auth/calendar.events https://www.googleapis.com/auth/calendar.readonly"), state, ) c.JSON(http.StatusOK, gin.H{"authUrl": authURL}) }) group.GET("/oauth/google-calendar/callback", func(c *gin.Context) { code := c.Query("code") state := c.Query("state") oauthState, exists := oauthStates[state] if !exists || oauthState.Provider != "google_calendar" { s.writeStatusError(c, http.StatusBadRequest, "invalid oauth state") return } delete(oauthStates, state) // Exchange code for tokens // In production, make actual HTTP request to Google's token endpoint // For now, we'll create a placeholder integration integration := s.store.CreateIntegration(store.CreateIntegrationInput{ WorkspaceSlug: oauthState.WorkspaceSlug, Provider: "google_calendar", Name: "Google Calendar", Config: `{"calendar_id": "primary"}`, Credentials: code, // In production, store actual tokens }) // Redirect back to the app c.Redirect(http.StatusTemporaryRedirect, oauthState.RedirectURL+"?connected=google_calendar") }) // Slack OAuth group.GET("/oauth/slack/connect", func(c *gin.Context) { workspaceSlug := c.Query("workspaceSlug") if _, ok := s.requireWorkspaceMember(c, workspaceSlug); !ok { return } state := uuid.NewString() redirectURL := c.Query("redirect") if redirectURL == "" { redirectURL = fmt.Sprintf("/app/%s/integrations", workspaceSlug) } oauthStates[state] = oauthState{ State: state, Provider: "slack", WorkspaceSlug: workspaceSlug, RedirectURL: redirectURL, } // Build Slack OAuth URL scopes := "chat:write,channels:read,groups:read,im:read" authURL := fmt.Sprintf( "https://slack.com/oauth/v2/authorize?client_id=%s&scope=%s&redirect_uri=%s&state=%s", url.QueryEscape(s.config.SlackClientID), url.QueryEscape(scopes), url.QueryEscape(s.config.SlackRedirectURI), state, ) c.JSON(http.StatusOK, gin.H{"authUrl": authURL}) }) group.GET("/oauth/slack/callback", func(c *gin.Context) { code := c.Query("code") state := c.Query("state") oauthState, exists := oauthStates[state] if !exists || oauthState.Provider != "slack" { s.writeStatusError(c, http.StatusBadRequest, "invalid oauth state") return } delete(oauthStates, state) // Exchange code for tokens // In production, make actual HTTP request to Slack's token endpoint // For now, we'll create a placeholder integration integration := s.store.CreateIntegration(store.CreateIntegrationInput{ WorkspaceSlug: oauthState.WorkspaceSlug, Provider: "slack", Name: "Slack", Config: `{"channel": "general"}`, Credentials: code, // In production, store actual tokens }) // Redirect back to the app c.Redirect(http.StatusTemporaryRedirect, oauthState.RedirectURL+"?connected=slack&integration_id="+integration.ID) }) // Disconnect integration group.POST("/integrations/:integrationId/disconnect", func(c *gin.Context) { integration, err := s.store.GetIntegrationByID(c.Param("integrationId")) if err != nil { s.writeStatusError(c, http.StatusNotFound, err.Error()) return } if _, ok := s.requireWorkspaceMember(c, integration.WorkspaceSlug); !ok { return } // In production, revoke OAuth tokens with the provider // For Google: https://oauth2.googleapis.com/revoke?token=... // For Slack: https://slack.com/api/auth.revoke?token=... if err := s.store.DeleteIntegration(integration.ID); err != nil { s.writeStatusError(c, http.StatusInternalServerError, err.Error()) return } c.JSON(http.StatusOK, gin.H{"ok": true}) }) } // SyncGoogleCalendar syncs events with Google Calendar func (s *Server) SyncGoogleCalendar(workspaceSlug string) error { integrations := s.store.ListIntegrations(workspaceSlug) for _, integration := range integrations { if integration.Provider == "google_calendar" && integration.Status == "active" { // In production, use the stored credentials to: // 1. Fetch events from Google Calendar // 2. Create/update events in our database // 3. Push local events to Google Calendar // This would be done via the Google Calendar API client } } return nil } // SendSlackNotification sends a notification to Slack func (s *Server) SendSlackNotification(workspaceSlug, channel, message string) error { integrations := s.store.ListIntegrations(workspaceSlug) for _, integration := range integrations { if integration.Provider == "slack" && integration.Status == "active" { // Parse config to get channel var config struct { Channel string `json:"channel"` } if err := json.Unmarshal([]byte(integration.Config), &config); err != nil { continue } // In production, use the stored credentials to: // 1. Post message to Slack channel via webhook or API // This would be done via the Slack API client } } return nil } // Helper to parse JSON config func parseConfig(configStr string) map[string]interface{} { var config map[string]interface{} if err := json.NewDecoder(strings.NewReader(configStr)).Decode(&config); err != nil { return make(map[string]interface{}) } return config }