feat(integrations): align notion authorization

This commit is contained in:
2026-06-06 18:00:19 +08:00
parent 5ed9952b7e
commit 0f2d64c8f6
9 changed files with 154 additions and 9 deletions
@@ -16,6 +16,7 @@ import (
"github.com/gochat/gochat/internal/model"
"github.com/gochat/gochat/internal/repository"
applogger "github.com/gochat/gochat/pkg/logger"
"github.com/golang-jwt/jwt/v5"
)
// LinearProviderError mirrors Chatwoot's `{ error: ... }`, 422 provider failures.
@@ -594,11 +595,48 @@ type NotionIntegrationService struct {
hookRepo *repository.IntegrationHookRepo
}
// NotionAuthorizationResponse mirrors Chatwoot's Notion authorization payload.
type NotionAuthorizationResponse struct {
Success bool `json:"success"`
URL string `json:"url,omitempty"`
}
// NewNotionIntegrationService creates a new NotionIntegrationService.
func NewNotionIntegrationService(hookRepo *repository.IntegrationHookRepo) *NotionIntegrationService {
return &NotionIntegrationService{hookRepo: hookRepo}
}
// BuildAuthorizationURL returns the Notion OAuth authorize URL for an account.
func (s *NotionIntegrationService) BuildAuthorizationURL(accountID uint) (*NotionAuthorizationResponse, error) {
clientID := strings.TrimSpace(os.Getenv("NOTION_CLIENT_ID"))
clientSecret := strings.TrimSpace(os.Getenv("NOTION_CLIENT_SECRET"))
if clientID == "" || clientSecret == "" {
return nil, fmt.Errorf("Notion OAuth is not configured")
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"sub": accountID,
"iat": time.Now().Unix(),
})
state, err := token.SignedString([]byte(clientSecret))
if err != nil {
return nil, fmt.Errorf("failed to generate Notion state: %w", err)
}
frontendURL := strings.TrimRight(os.Getenv("FRONTEND_URL"), "/")
if frontendURL == "" {
frontendURL = "http://localhost:3000"
}
params := url.Values{}
params.Set("client_id", clientID)
params.Set("owner", "user")
params.Set("redirect_uri", frontendURL+"/notion/callback")
params.Set("response_type", "code")
params.Set("state", state)
return &NotionAuthorizationResponse{Success: true, URL: "https://api.notion.com/v1/oauth/authorize?" + params.Encode()}, nil
}
// Delete removes a Notion integration hook for an account.
func (s *NotionIntegrationService) Delete(ctx context.Context, accountID uint) error {
hooks, err := s.findNotionHooks(ctx, accountID)
@@ -6,9 +6,11 @@ import (
"encoding/json"
"io"
"net/http"
"net/url"
"strings"
"testing"
"github.com/golang-jwt/jwt/v5"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gorm.io/driver/sqlite"
@@ -370,6 +372,48 @@ func TestLinearIntegrationService_GetLinkedIssues_UsesConversationDisplayID(t *t
// NotionIntegrationService tests
// ========================================
func TestNotionIntegrationService_BuildAuthorizationURL(t *testing.T) {
t.Setenv("NOTION_CLIENT_ID", "notion-client")
t.Setenv("NOTION_CLIENT_SECRET", "notion-secret")
t.Setenv("FRONTEND_URL", "https://app.example.test/")
svc, _ := setupNotionService(t)
resp, err := svc.BuildAuthorizationURL(42)
require.NoError(t, err)
require.NotNil(t, resp)
assert.True(t, resp.Success)
parsed, err := url.Parse(resp.URL)
require.NoError(t, err)
assert.Equal(t, "https", parsed.Scheme)
assert.Equal(t, "api.notion.com", parsed.Host)
assert.Equal(t, "/v1/oauth/authorize", parsed.Path)
query := parsed.Query()
assert.Equal(t, "notion-client", query.Get("client_id"))
assert.Equal(t, "code", query.Get("response_type"))
assert.Equal(t, "user", query.Get("owner"))
assert.Equal(t, "https://app.example.test/notion/callback", query.Get("redirect_uri"))
claims := jwt.MapClaims{}
token, err := jwt.ParseWithClaims(query.Get("state"), claims, func(token *jwt.Token) (any, error) {
return []byte("notion-secret"), nil
})
require.NoError(t, err)
require.True(t, token.Valid)
assert.Equal(t, float64(42), claims["sub"])
}
func TestNotionIntegrationService_BuildAuthorizationURL_NotConfigured(t *testing.T) {
t.Setenv("NOTION_CLIENT_ID", "")
t.Setenv("NOTION_CLIENT_SECRET", "")
svc, _ := setupNotionService(t)
resp, err := svc.BuildAuthorizationURL(42)
assert.Nil(t, resp)
assert.Error(t, err)
assert.Contains(t, err.Error(), "Notion OAuth is not configured")
}
func TestNotionIntegrationService_Delete_Success(t *testing.T) {
svc, db := setupNotionService(t)
accountID := seedLinearNotionAccount(db, t)