mirror of
https://github.com/Dvorinka/Productier.git
synced 2026-06-04 20:43:02 +00:00
first commit
This commit is contained in:
@@ -0,0 +1,212 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user