collavre_notion 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 80b38ec1b831d76c7062b71483225adb5985e90ea4abb7d6b29023e53182c299
4
+ data.tar.gz: a57dd0b62793e9129f3a0190d4f42cf86c5687b7c39737ec220cd0e5fad90664
5
+ SHA512:
6
+ metadata.gz: 19697a81efd966dc00d0290f4d827dc48a471dcf3e65d4d8e5b0e618830bf9ac237ac67181d74ee75a82ad648aa77d863521357183fbcebe318955f62dd38550
7
+ data.tar.gz: acbfc93ef56e9ac9955a988ed204cfb4f39230c096d555cef7f62115f908d9b53332dc4062914fe39ab939afc01f659c92c501d5994a0494efbe3aae4da061b8
data/README.md ADDED
@@ -0,0 +1,58 @@
1
+ # Collavre Notion Engine
2
+
3
+ Notion integration plugin engine for Collavre. Provides OAuth connection, page linking, and creative export/sync to Notion.
4
+
5
+ ## Documentation
6
+
7
+ - [Setup Guide](docs/SETUP.md) - Step-by-step instructions for creating and configuring a Notion integration
8
+ - [Architecture](docs/ARCHITECTURE.md) - Technical documentation about the integration
9
+
10
+ ## Installation
11
+
12
+ ### 1. Add to Gemfile
13
+
14
+ ```ruby
15
+ # Gemfile
16
+ gem "collavre_notion", path: "engines/collavre_notion"
17
+ ```
18
+
19
+ ### 2. Add JavaScript Import (Required)
20
+
21
+ The JavaScript must be explicitly imported in your host application:
22
+
23
+ ```javascript
24
+ // app/javascript/application.js
25
+ import "collavre_notion"
26
+ ```
27
+
28
+ This is required because the host app controls which integrations are loaded. Without this import, the Notion integration modal will not work.
29
+
30
+ ### 3. Configure Environment Variables
31
+
32
+ | Variable | Required | Description |
33
+ |----------|----------|-------------|
34
+ | `NOTION_CLIENT_ID` | Yes | OAuth Client ID |
35
+ | `NOTION_CLIENT_SECRET` | Yes | OAuth Client Secret |
36
+
37
+ ### 4. Run Migrations
38
+
39
+ ```bash
40
+ rails db:migrate
41
+ ```
42
+
43
+ ## Automatic Configuration
44
+
45
+ The following are automatically configured by the engine:
46
+
47
+ - **Routes**: Mounted at `/notion` (no manual `mount` needed in `routes.rb`)
48
+ - **OAuth Callback**: Handles `/auth/notion/callback`
49
+ - **Migrations**: Database migrations are automatically included
50
+ - **i18n**: Locale files (en, ko) are automatically loaded
51
+ - **Integration Registry**: Automatically registers with `Collavre::IntegrationRegistry`
52
+
53
+ ## Features
54
+
55
+ - OAuth 2.0 connection to Notion workspaces
56
+ - Export a creative tree to a Notion page
57
+ - Sync linked pages with updated creative content
58
+ - Manage linked page connections per creative
data/Rakefile ADDED
@@ -0,0 +1,3 @@
1
+ require_relative "lib/collavre_notion"
2
+
3
+ CollavreNotion::Engine.load_tasks
@@ -0,0 +1,12 @@
1
+ module CollavreNotion
2
+ class ApplicationController < ::ApplicationController
3
+ protect_from_forgery with: :exception
4
+
5
+ private
6
+
7
+ def notion_engine
8
+ CollavreNotion::Engine.routes.url_helpers
9
+ end
10
+ helper_method :notion_engine
11
+ end
12
+ end
@@ -0,0 +1,196 @@
1
+ module CollavreNotion
2
+ module Creatives
3
+ class NotionIntegrationsController < ApplicationController
4
+ before_action :set_creative
5
+ before_action :ensure_read_permission
6
+ before_action :ensure_admin_permission, only: [ :show, :update ]
7
+
8
+ def show
9
+ account = Current.user.notion_account
10
+ links = linked_page_links(account)
11
+
12
+ Rails.logger.info("Notion Integration: Showing status for user #{Current.user.id}, connected: #{account.present?}")
13
+
14
+ render json: {
15
+ connected: account.present?,
16
+ creative_title: helpers.strip_tags(@creative.description).strip.presence || "Untitled Creative",
17
+ account: account && {
18
+ workspace_name: account.workspace_name,
19
+ workspace_id: account.workspace_id,
20
+ bot_id: account.bot_id
21
+ },
22
+ linked_pages: links.map do |link|
23
+ {
24
+ page_id: link.page_id,
25
+ page_title: link.page_title,
26
+ page_url: link.page_url,
27
+ last_synced_at: link.last_synced_at
28
+ }
29
+ end,
30
+ available_pages: account.present? ? fetch_available_pages(account) : []
31
+ }
32
+ end
33
+
34
+ def update
35
+ account = Current.user.notion_account
36
+ unless account
37
+ render json: { error: "not_connected" }, status: :unprocessable_entity
38
+ return
39
+ end
40
+
41
+ Rails.logger.info("Notion Integration Update: Full params = #{params.to_unsafe_h}")
42
+
43
+ integration_attributes = integration_params
44
+ Rails.logger.info("Notion Integration Update: integration_params = #{integration_attributes}")
45
+
46
+ request_action = request.request_parameters[:action]
47
+ action = integration_attributes[:action]
48
+ if action.blank? || action.to_s == action_name
49
+ action = request_action.presence
50
+ end
51
+ parent_page_id = integration_attributes[:parent_page_id]
52
+
53
+ Rails.logger.info("Notion Integration Update: action=#{action}, parent_page_id=#{parent_page_id}")
54
+
55
+ begin
56
+ case action
57
+ when "export"
58
+ # Export creative to Notion
59
+ CollavreNotion::NotionExportJob.perform_later(@creative, account, parent_page_id)
60
+ render json: { success: true, message: "Export started" }
61
+ when "sync"
62
+ # Sync existing page
63
+ link = linked_page_links(account).first
64
+ if link
65
+ CollavreNotion::NotionSyncJob.perform_later(@creative, account, link.page_id)
66
+ render json: { success: true, message: "Sync started" }
67
+ else
68
+ render json: { error: "no_linked_page" }, status: :unprocessable_entity
69
+ end
70
+ else
71
+ render json: { error: "invalid_action" }, status: :unprocessable_entity
72
+ end
73
+ rescue StandardError => e
74
+ Rails.logger.error("Notion integration error: #{e.message}")
75
+ render json: { error: "operation_failed", message: e.message }, status: :internal_server_error
76
+ end
77
+ end
78
+
79
+ def destroy
80
+ unless @creative.has_permission?(Current.user, :write)
81
+ render json: { error: "forbidden" }, status: :forbidden
82
+ return
83
+ end
84
+
85
+ account = Current.user.notion_account
86
+ unless account
87
+ render json: { error: "not_connected" }, status: :unprocessable_entity
88
+ return
89
+ end
90
+
91
+ page_id = params[:page_id]
92
+
93
+ if page_id
94
+ # Remove specific page link
95
+ link = linked_page_links(account).find_by(page_id: page_id)
96
+ unless link
97
+ render json: { error: "not_found" }, status: :not_found
98
+ return
99
+ end
100
+
101
+ link.destroy!
102
+ else
103
+ # Remove all page links for this creative
104
+ linked_page_links(account).destroy_all
105
+ end
106
+
107
+ links = linked_page_links(account)
108
+ render json: {
109
+ success: true,
110
+ linked_pages: links.map do |link|
111
+ {
112
+ page_id: link.page_id,
113
+ page_title: link.page_title,
114
+ page_url: link.page_url,
115
+ last_synced_at: link.last_synced_at
116
+ }
117
+ end
118
+ }
119
+ end
120
+
121
+ private
122
+
123
+ def set_creative
124
+ @creative = Collavre::Creative.find(params[:creative_id])
125
+ end
126
+
127
+ def ensure_read_permission
128
+ return if @creative.has_permission?(Current.user, :read)
129
+
130
+ render json: { error: "forbidden" }, status: :forbidden
131
+ end
132
+
133
+ def ensure_admin_permission
134
+ return if @creative.has_permission?(Current.user, :admin)
135
+
136
+ render json: { error: "forbidden" }, status: :forbidden
137
+ end
138
+
139
+ def linked_page_links(account)
140
+ return CollavreNotion::NotionPageLink.none unless account
141
+
142
+ @creative.notion_page_links.where(notion_account: account)
143
+ end
144
+
145
+ def integration_params
146
+ permitted_keys = [ :action, :parent_page_id, :page_id ]
147
+
148
+ if params[:notion_integration].present?
149
+ params.require(:notion_integration).permit(*permitted_keys)
150
+ else
151
+ params.permit(*permitted_keys)
152
+ end
153
+ end
154
+
155
+ def fetch_available_pages(account)
156
+ Rails.logger.info("Notion Integration: Fetching available pages for account #{account.id}")
157
+
158
+ begin
159
+ service = CollavreNotion::NotionService.new(user: account.user)
160
+ pages_response = service.search_pages(page_size: 50)
161
+
162
+ Rails.logger.info("Notion Integration: Search pages response - #{pages_response["results"]&.length || 0} pages found")
163
+
164
+ pages = pages_response["results"]&.map do |page|
165
+ {
166
+ id: page["id"],
167
+ title: extract_page_title(page),
168
+ url: page["url"],
169
+ parent: page["parent"]
170
+ }
171
+ end || []
172
+
173
+ Rails.logger.info("Notion Integration: Returning #{pages.length} formatted pages")
174
+ pages
175
+ rescue StandardError => e
176
+ Rails.logger.error("Notion Integration: Error fetching pages - #{e.class}: #{e.message}")
177
+ Rails.logger.error(e.backtrace.join("\n"))
178
+ []
179
+ end
180
+ end
181
+
182
+ def extract_page_title(page)
183
+ # Handle different title property structures
184
+ title_property = page.dig("properties", "title")
185
+
186
+ if title_property && title_property["title"]
187
+ title_property["title"].map { |t| t.dig("text", "content") }.compact.join("")
188
+ elsif page["title"]
189
+ page["title"].map { |t| t.dig("text", "content") }.compact.join("")
190
+ else
191
+ "Untitled"
192
+ end
193
+ end
194
+ end
195
+ end
196
+ end
@@ -0,0 +1,48 @@
1
+ module CollavreNotion
2
+ class NotionAuthController < ApplicationController
3
+ allow_unauthenticated_access only: :callback
4
+ before_action -> { enforce_auth_provider!(:notion) }, only: :callback
5
+
6
+ def callback
7
+ auth = request.env["omniauth.auth"]
8
+ notion = CollavreNotion::NotionAccount.find_or_initialize_by(notion_uid: auth.uid)
9
+
10
+ if notion.new_record?
11
+ unless Current.user
12
+ redirect_to collavre.new_session_path, alert: I18n.t("collavre_notion.notion_auth.login_first")
13
+ return
14
+ end
15
+ notion.user = Current.user
16
+ end
17
+
18
+ notion.token = auth.credentials.token
19
+ notion.workspace_name = auth.info.name
20
+ notion.save!
21
+
22
+ # If opened in popup, close it and notify parent window
23
+ if params[:popup] || request.referer&.include?("popup=true") || session[:oauth_popup]
24
+ session.delete(:oauth_popup)
25
+ render html: <<-HTML.html_safe
26
+ <!DOCTYPE html>
27
+ <html>
28
+ <head><title>Notion Connected</title></head>
29
+ <body>
30
+ <script>
31
+ if (window.opener) {
32
+ window.opener.postMessage({ type: 'notion_oauth_success', connected: true }, '*');
33
+ window.close();
34
+ } else {
35
+ window.location.href = '#{collavre.creatives_path}';
36
+ }
37
+ </script>
38
+ <p>#{I18n.t("collavre_notion.notion_auth.connected")}</p>
39
+ <p>This window should close automatically. If not, you can close it manually.</p>
40
+ </body>
41
+ </html>
42
+ HTML
43
+ else
44
+ redirect_to collavre.creatives_path, notice: I18n.t("collavre_notion.notion_auth.connected")
45
+ end
46
+ end
47
+ end
48
+ end