mirror of
https://github.com/Dvorinka/Productier.git
synced 2026-06-04 04:23:00 +00:00
212 lines
6.6 KiB
Go
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
|
|
} |