Files
Tomas Dvorak 3cb40adb23 first commit
2026-04-10 12:04:09 +02:00

212 lines
6.6 KiB
Go

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
}