package v1 import ( "fmt" "net/url" "strings" "time" "github.com/gin-gonic/gin" "github.com/golang-jwt/jwt/v5" "github.com/gochat/gochat/internal/oauth/credentialstore" ) const instagramAuthorizationScope = "instagram_business_basic,instagram_business_manage_messages" const tiktokAuthorizationScope = "user.info.basic,user.info.username,user.info.stats,user.info.profile,user.account.type,user.insights,message.list.read,message.list.send,message.list.manage" func authorizationReturnTo(c *gin.Context, bodyValue string) string { if value := strings.TrimSpace(c.Query("return_to")); value != "" { return value } return strings.TrimSpace(bodyValue) } // TikTokAuthorizationRequest is the DTO for initiating TikTok OAuth flow // with per-inbox credentials. type TikTokAuthorizationRequest struct { AppID string `json:"app_id"` AppSecret string `json:"app_secret"` ReturnTo string `json:"return_to"` } func buildInstagramChatwootAuthorizationURL(accountID uint, returnTo string, appID, appSecret string) (string, error) { if strings.TrimSpace(appID) == "" || strings.TrimSpace(appSecret) == "" { return "", fmt.Errorf("Instagram App ID and Secret are required") } // Store credentials temporarily for the callback to retrieve nonce, err := credentialstore.Store(appID, appSecret) if err != nil { return "", fmt.Errorf("failed to store OAuth credentials: %w", err) } state, err := signedChatwootOAuthStateWithReturnToAndNonce(accountID, appSecret, returnTo, nonce) if err != nil { return "", err } frontendURL := strings.TrimRight(envOrDefaultV1("FRONTEND_URL", "http://localhost:3000"), "/") params := url.Values{} params.Set("client_id", appID) params.Set("redirect_uri", frontendURL+"/instagram/callback") params.Set("scope", instagramAuthorizationScope) params.Set("enable_fb_login", "0") params.Set("force_authentication", "1") params.Set("response_type", "code") params.Set("state", state) return "https://api.instagram.com/oauth/authorize?" + params.Encode(), nil } func buildTikTokChatwootAuthorizationURL(accountID uint, returnTo string, appID, appSecret string) (string, error) { if strings.TrimSpace(appID) == "" || strings.TrimSpace(appSecret) == "" { return "", fmt.Errorf("TikTok App ID and Secret are required") } // Store credentials temporarily for the callback to retrieve nonce, err := credentialstore.Store(appID, appSecret) if err != nil { return "", fmt.Errorf("failed to store OAuth credentials: %w", err) } state, err := signedChatwootOAuthStateWithReturnToAndNonce(accountID, appSecret, returnTo, nonce) if err != nil { return "", err } frontendURL := strings.TrimRight(envOrDefaultV1("FRONTEND_URL", "http://localhost:3000"), "/") params := url.Values{} params.Set("client_id", appID) params.Set("client_key", appID) params.Set("redirect_uri", frontendURL+"/tiktok/callback") params.Set("response_type", "code") params.Set("scope", tiktokAuthorizationScope) params.Set("state", state) return "https://www.tiktok.com/v2/auth/authorize?" + params.Encode(), nil } // signedChatwootOAuthStateWithReturnToAndNonce creates a JWT state token // that includes a credential nonce for the callback to retrieve stored credentials. func signedChatwootOAuthStateWithReturnToAndNonce(accountID uint, secret string, returnTo string, nonce string) (string, error) { claims := jwt.MapClaims{ "sub": accountID, "iat": time.Now().Unix(), "nonce": nonce, } if strings.TrimSpace(returnTo) != "" { claims["return_to"] = strings.TrimSpace(returnTo) } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) return token.SignedString([]byte(secret)) }