@resultcrafter/aimanager-instagram-connector 0.1.2 → 0.2.1

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.
@@ -0,0 +1,218 @@
1
+ var crypto = require('crypto');
2
+ var axios = require('axios');
3
+
4
+ var INSTAGRAM_OAUTH_BASE = 'https://www.instagram.com/oauth';
5
+ var INSTAGRAM_API_BASE = 'https://api.instagram.com/oauth';
6
+ var INSTAGRAM_GRAPH_BASE = 'https://graph.instagram.com';
7
+
8
+ function getConfig() {
9
+ return {
10
+ appId: process.env.INSTAGRAM_APP_ID,
11
+ appSecret: process.env.INSTAGRAM_APP_SECRET,
12
+ redirectUri: process.env.INSTAGRAM_REDIRECT_URI,
13
+ stateSecret: process.env.OAUTH_STATE_SECRET || process.env.INSTAGRAM_APP_SECRET || 'default_secret',
14
+ scopes: ['instagram_business_basic', 'instagram_business_manage_messages'].join(',')
15
+ };
16
+ }
17
+
18
+ function getEncryptionKey() {
19
+ return process.env.TOKEN_ENCRYPTION_KEY || process.env.OAUTH_STATE_SECRET || null;
20
+ }
21
+
22
+ function getCipherKey() {
23
+ var key = getEncryptionKey();
24
+ if (!key) return null;
25
+ return crypto.createHash('sha256').update(key).digest();
26
+ }
27
+
28
+ function encrypt(data) {
29
+ var cipherKey = getCipherKey();
30
+ if (!cipherKey) return null;
31
+ var text = JSON.stringify(data);
32
+ var iv = crypto.randomBytes(16);
33
+ var cipher = crypto.createCipheriv('aes-256-gcm', cipherKey, iv);
34
+ var encrypted = Buffer.concat([cipher.update(text, 'utf8'), cipher.final()]);
35
+ var authTag = cipher.getAuthTag();
36
+ return {
37
+ encrypted: encrypted.toString('base64'),
38
+ iv: iv.toString('base64'),
39
+ authTag: authTag.toString('base64')
40
+ };
41
+ }
42
+
43
+ function decrypt(encryptedData) {
44
+ var cipherKey = getCipherKey();
45
+ if (!cipherKey) return null;
46
+ var decipher = crypto.createDecipheriv('aes-256-gcm', cipherKey, Buffer.from(encryptedData.iv, 'base64'));
47
+ decipher.setAuthTag(Buffer.from(encryptedData.authTag, 'base64'));
48
+ var decrypted = Buffer.concat([
49
+ decipher.update(Buffer.from(encryptedData.encrypted, 'base64')),
50
+ decipher.final()
51
+ ]);
52
+ return JSON.parse(decrypted.toString('utf8'));
53
+ }
54
+
55
+ function encryptToken(tokenData) {
56
+ var encrypted = encrypt(tokenData);
57
+ if (encrypted) return encrypted;
58
+ return tokenData;
59
+ }
60
+
61
+ function decryptToken(stored) {
62
+ if (!stored) return null;
63
+ if (stored.encrypted && stored.iv && stored.authTag) {
64
+ var decrypted = decrypt(stored);
65
+ if (decrypted) return decrypted;
66
+ }
67
+ return stored;
68
+ }
69
+
70
+ function generateState(sessionId, projectId) {
71
+ var data = {
72
+ sessionId: sessionId || crypto.randomBytes(16).toString('hex'),
73
+ project_id: projectId,
74
+ timestamp: Date.now(),
75
+ nonce: crypto.randomBytes(8).toString('hex')
76
+ };
77
+ var cfg = getConfig();
78
+ var hmac = crypto.createHmac('sha256', cfg.stateSecret);
79
+ hmac.update(JSON.stringify(data));
80
+ var state = Buffer.from(JSON.stringify({
81
+ data: data,
82
+ signature: hmac.digest('hex')
83
+ })).toString('base64url');
84
+ return state;
85
+ }
86
+
87
+ function verifyState(state) {
88
+ try {
89
+ var cfg = getConfig();
90
+ var decoded = JSON.parse(Buffer.from(state, 'base64url').toString());
91
+ var hmac = crypto.createHmac('sha256', cfg.stateSecret);
92
+ hmac.update(JSON.stringify(decoded.data));
93
+ if (decoded.signature !== hmac.digest('hex')) {
94
+ return null;
95
+ }
96
+ if (Date.now() - decoded.data.timestamp > 600000) return null;
97
+ return decoded.data;
98
+ } catch (e) {
99
+ return null;
100
+ }
101
+ }
102
+
103
+ function getAuthorizationUrl(projectId, token, appId) {
104
+ var cfg = getConfig();
105
+ if (!cfg.appId) throw new Error('INSTAGRAM_APP_ID not configured');
106
+ if (!cfg.redirectUri) throw new Error('INSTAGRAM_REDIRECT_URI not configured');
107
+ var state = generateState(null, projectId);
108
+ var params = new URLSearchParams({
109
+ client_id: cfg.appId,
110
+ redirect_uri: cfg.redirectUri,
111
+ scope: cfg.scopes,
112
+ response_type: 'code',
113
+ state: state
114
+ });
115
+ return { url: INSTAGRAM_OAUTH_BASE + '/authorize?' + params.toString(), state: state };
116
+ }
117
+
118
+ async function exchangeCodeForToken(code) {
119
+ var cfg = getConfig();
120
+ if (!cfg.appId || !cfg.appSecret) throw new Error('Instagram app credentials not configured');
121
+ var tokenRes = await axios.post(INSTAGRAM_API_BASE + '/access_token',
122
+ new URLSearchParams({
123
+ client_id: cfg.appId,
124
+ client_secret: cfg.appSecret,
125
+ grant_type: 'authorization_code',
126
+ redirect_uri: cfg.redirectUri,
127
+ code: code
128
+ }),
129
+ { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }
130
+ );
131
+ var shortToken = tokenRes.data.access_token;
132
+ var userId = tokenRes.data.user_id;
133
+ var longRes = await axios.get(INSTAGRAM_GRAPH_BASE + '/access_token', {
134
+ params: { grant_type: 'ig_exchange_token', client_secret: cfg.appSecret, access_token: shortToken }
135
+ });
136
+ var longToken = longRes.data.access_token;
137
+ var userInfo = await getUserInfo(longToken);
138
+ return {
139
+ access_token: longToken,
140
+ token_type: longRes.data.token_type,
141
+ expires_in: longRes.data.expires_in,
142
+ user_id: userId,
143
+ instagram_business_id: userInfo.instagram_business_id || userInfo.user_id || userId,
144
+ username: userInfo.username,
145
+ user_info: userInfo,
146
+ created_at: Date.now(),
147
+ expires_at: Date.now() + (longRes.data.expires_in * 1000)
148
+ };
149
+ }
150
+
151
+ async function refreshToken(currentToken, existingTokenData) {
152
+ var cfg = getConfig();
153
+ var res = await axios.get(INSTAGRAM_GRAPH_BASE + '/refresh_access_token', {
154
+ params: { grant_type: 'ig_refresh_token', access_token: currentToken }
155
+ });
156
+ return {
157
+ access_token: res.data.access_token,
158
+ token_type: res.data.token_type,
159
+ expires_in: res.data.expires_in,
160
+ user_id: existingTokenData ? existingTokenData.user_id : undefined,
161
+ instagram_business_id: existingTokenData ? existingTokenData.instagram_business_id : undefined,
162
+ username: existingTokenData ? existingTokenData.username : undefined,
163
+ user_info: existingTokenData ? existingTokenData.user_info : undefined,
164
+ created_at: Date.now(),
165
+ expires_at: Date.now() + (res.data.expires_in * 1000)
166
+ };
167
+ }
168
+
169
+ async function getUserInfo(accessToken) {
170
+ try {
171
+ var res = await axios.get(INSTAGRAM_GRAPH_BASE + '/v23.0/me', {
172
+ params: {
173
+ fields: 'id,user_id,username,name,account_type,media_count,followers_count,follows_count,profile_picture_url',
174
+ access_token: accessToken
175
+ }
176
+ });
177
+ return {
178
+ app_scoped_id: res.data.id,
179
+ instagram_business_id: res.data.user_id,
180
+ username: res.data.username,
181
+ name: res.data.name,
182
+ account_type: res.data.account_type,
183
+ profile_picture_url: res.data.profile_picture_url
184
+ };
185
+ } catch (e) {
186
+ return { username: null };
187
+ }
188
+ }
189
+
190
+ async function getTokenStatus(db, projectId) {
191
+ var settings = await db.get('instagram-' + projectId);
192
+ if (!settings || !settings.ig_oauth_token) {
193
+ return { connected: false, instagram_username: null };
194
+ }
195
+ var token = decryptToken(settings.ig_oauth_token);
196
+ if (!token) {
197
+ return { connected: false, instagram_username: null };
198
+ }
199
+ var daysRemaining = token.expires_at ? Math.floor((token.expires_at - Date.now()) / (1000 * 60 * 60 * 24)) : null;
200
+ return {
201
+ connected: true,
202
+ instagram_username: token.username || settings.instagram_username || 'Instagram Account',
203
+ user_id: token.instagram_business_id || token.user_id,
204
+ days_remaining: daysRemaining,
205
+ needs_refresh: daysRemaining !== null ? daysRemaining < 7 : false
206
+ };
207
+ }
208
+
209
+ module.exports = {
210
+ getAuthorizationUrl: getAuthorizationUrl,
211
+ exchangeCodeForToken: exchangeCodeForToken,
212
+ refreshToken: refreshToken,
213
+ getUserInfo: getUserInfo,
214
+ verifyState: verifyState,
215
+ getTokenStatus: getTokenStatus,
216
+ encryptToken: encryptToken,
217
+ decryptToken: decryptToken
218
+ };
package/index.js CHANGED
@@ -5,6 +5,7 @@ const bodyParser = require("body-parser");
5
5
  const handlebars = require('handlebars');
6
6
  const path = require('path');
7
7
  const fs = require('fs');
8
+ const crypto = require('crypto');
8
9
  const pjson = require('./package.json');
9
10
  const winston = require('./winston');
10
11
  const url = require('url');
@@ -16,6 +17,7 @@ const { TiledeskInstagramTranslator } = require('./aimanager/TiledeskInstagramTr
16
17
  const { TiledeskSubscriptionClient } = require('./aimanager/TiledeskSubscriptionClient');
17
18
  const { FacebookClient } = require('./aimanager/FacebookClient');
18
19
  const { MessageHandler } = require('./aimanager/MessageHandler');
20
+ const instagramOAuth = require('./aimanager/InstagramOAuth');
19
21
 
20
22
 
21
23
  // mongo
@@ -110,13 +112,14 @@ router.post('/install', async (req, res) => {
110
112
  query: {
111
113
  "project_id": project_id,
112
114
  "app_id": app_id,
113
- "token": token
115
+ "token": token,
116
+ "oauth_url": BASE_URL + '/auth/instagram/start'
114
117
  }
115
118
  }));
116
119
 
117
120
  }).catch((err) => {
118
- winston.error("(fbm) installation error: ", err.data)
119
- winston.error("(fbm) installation error: " + err.data)
121
+ winston.error("(ig) installation error: ", err.data)
122
+ winston.error("(ig) installation error: " + err.data)
120
123
  res.send("An error occurred during the installation");
121
124
  })
122
125
 
@@ -184,6 +187,8 @@ router.get('/configure', async (req, res) => {
184
187
  return []
185
188
  })
186
189
 
190
+ var tokenStatus = await instagramOAuth.getTokenStatus(db, project_id);
191
+
187
192
  if (settings) {
188
193
  winston.debug("(fbm) settings found: ", settings);
189
194
 
@@ -192,7 +197,7 @@ router.get('/configure', async (req, res) => {
192
197
  var replacements = {
193
198
  app_version: pjson.version,
194
199
  project_id: project_id,
195
- ig_token: settings.ig_token || settings.access_token,
200
+ ig_token: tokenStatus.connected ? tokenStatus.instagram_username : null,
196
201
  token: token,
197
202
  app_id: app_id,
198
203
  endpoint: BASE_URL,
@@ -201,9 +206,10 @@ router.get('/configure', async (req, res) => {
201
206
  department_id: settings.department_id,
202
207
  departments: departments,
203
208
  brand_name: BRAND_NAME,
204
- instagram_username: settings.instagram_username || settings.user_info?.username || 'Instagram Account',
209
+ instagram_username: tokenStatus.instagram_username || 'Instagram Account',
205
210
  show_info_message: settings.show_info_message !== undefined ? settings.show_info_message : true,
206
- subscription_id: settings.subscription_id
211
+ subscription_id: settings.subscription_id,
212
+ oauth_url: BASE_URL + '/auth/instagram/start?project_id=' + project_id + '&token=' + token + '&app_id=' + app_id
207
213
  }
208
214
  var html = template(replacements)
209
215
  res.send(html);
@@ -221,7 +227,8 @@ router.get('/configure', async (req, res) => {
221
227
  app_id: app_id,
222
228
  endpoint: BASE_URL,
223
229
  departments: departments,
224
- brand_name: BRAND_NAME
230
+ brand_name: BRAND_NAME,
231
+ oauth_url: BASE_URL + '/auth/instagram/start?project_id=' + project_id + '&token=' + token + '&app_id=' + app_id
225
232
  }
226
233
  var html = template(replacements);
227
234
  res.send(html);
@@ -232,7 +239,7 @@ router.get('/configure', async (req, res) => {
232
239
  })
233
240
 
234
241
  router.post('/update', async (req, res) => {
235
- winston.verbose("(fbm) /update");
242
+ winston.verbose("(ig) /update");
236
243
 
237
244
  let project_id = req.body.project_id;
238
245
  let token = req.body.token;
@@ -247,10 +254,11 @@ router.post('/update', async (req, res) => {
247
254
  await db.set(CONTENT_KEY, settings);
248
255
  }
249
256
 
250
- // get departments
257
+ var tokenStatus = await instagramOAuth.getTokenStatus(db, project_id);
258
+
251
259
  const tdChannel = new TiledeskChannel({ settings: { project_id: project_id, token: token }, API_URL: API_URL });
252
260
  let departments = await tdChannel.getDepartments(token);
253
- winston.verbose("(fbm) found " + departments.length + " departments")
261
+ winston.verbose("(ig) found " + departments.length + " departments")
254
262
 
255
263
  readHTMLFile('/configure.html', (err, html) => {
256
264
 
@@ -258,7 +266,7 @@ router.post('/update', async (req, res) => {
258
266
  var replacements = {
259
267
  app_version: pjson.version,
260
268
  project_id: project_id,
261
- ig_token: settings ? (settings.ig_token || settings.access_token) : null,
269
+ ig_token: tokenStatus.connected ? tokenStatus.instagram_username : null,
262
270
  token: token,
263
271
  app_id: app_id,
264
272
  endpoint: BASE_URL,
@@ -268,8 +276,9 @@ router.post('/update', async (req, res) => {
268
276
  departments: departments,
269
277
  brand_name: BRAND_NAME,
270
278
  show_info_message: settings ? (settings.show_info_message !== undefined ? settings.show_info_message : true) : true,
271
- instagram_username: settings ? (settings.instagram_username || settings.user_info?.username || 'Instagram Account') : null,
272
- subscription_id: settings ? settings.subscription_id : null
279
+ instagram_username: tokenStatus.instagram_username || 'Instagram Account',
280
+ subscription_id: settings ? settings.subscription_id : null,
281
+ oauth_url: BASE_URL + '/auth/instagram/start?project_id=' + project_id + '&token=' + token + '&app_id=' + app_id
273
282
  }
274
283
  var html = template(replacements)
275
284
  res.send(html);
@@ -278,7 +287,7 @@ router.post('/update', async (req, res) => {
278
287
  })
279
288
 
280
289
  router.post('/update_advanced', async (req, res) => {
281
- winston.verbose("(fbm) /update_advanced");
290
+ winston.verbose("(ig) /update_advanced");
282
291
 
283
292
  let project_id = req.body.project_id;
284
293
  let show_info_message = req.body.show_info_message === 'on';
@@ -291,6 +300,8 @@ router.post('/update_advanced', async (req, res) => {
291
300
  await db.set(CONTENT_KEY, settings);
292
301
  }
293
302
 
303
+ var tokenStatus = await instagramOAuth.getTokenStatus(db, project_id);
304
+
294
305
  const tdChannel = new TiledeskChannel({ settings: { project_id: project_id, token: req.body.token }, API_URL: API_URL });
295
306
  let departments = await tdChannel.getDepartments(req.body.token).catch(() => []);
296
307
 
@@ -299,7 +310,7 @@ router.post('/update_advanced', async (req, res) => {
299
310
  var replacements = {
300
311
  app_version: pjson.version,
301
312
  project_id: project_id,
302
- ig_token: settings ? (settings.ig_token || settings.access_token) : null,
313
+ ig_token: tokenStatus.connected ? tokenStatus.instagram_username : null,
303
314
  token: req.body.token,
304
315
  endpoint: BASE_URL,
305
316
  pages: settings ? settings.pages : null,
@@ -308,8 +319,9 @@ router.post('/update_advanced', async (req, res) => {
308
319
  departments: departments,
309
320
  brand_name: BRAND_NAME,
310
321
  show_info_message: show_info_message,
311
- instagram_username: settings ? (settings.instagram_username || settings.user_info?.username || 'Instagram Account') : null,
312
- subscription_id: settings ? settings.subscription_id : null
322
+ instagram_username: tokenStatus.instagram_username || 'Instagram Account',
323
+ subscription_id: settings ? settings.subscription_id : null,
324
+ oauth_url: BASE_URL + '/auth/instagram/start?project_id=' + project_id + '&token=' + req.body.token
313
325
  }
314
326
  var html = template(replacements)
315
327
  res.send(html);
@@ -501,12 +513,45 @@ router.post('/tiledesk', async (req, res) => {
501
513
  return res.status(200).send({ message: "sent" });
502
514
  })
503
515
 
516
+ function verifyWebhookSignature(req) {
517
+ var signature = req.headers['x-hub-signature-256'];
518
+ if (!signature) {
519
+ winston.error("(ig) Missing x-hub-signature-256 header");
520
+ return false;
521
+ }
522
+ if (!APP_SECRET) {
523
+ winston.warn("(ig) APP_SECRET not configured — skipping signature validation");
524
+ return true;
525
+ }
526
+ var algo = 'sha256';
527
+ var expected = signature.replace('sha256=', '');
528
+ var hmac = crypto.createHmac(algo, APP_SECRET);
529
+ hmac.update(JSON.stringify(req.body));
530
+ var computed = hmac.digest('hex');
531
+ var valid = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(computed));
532
+ if (!valid) {
533
+ winston.error("(ig) Webhook signature mismatch — expected=" + expected + " computed=" + computed);
534
+ if (process.env.DEBUG_SIGNATURES === 'true') {
535
+ winston.debug("(ig) Raw body for signature: " + JSON.stringify(req.body));
536
+ }
537
+ }
538
+ return valid;
539
+ }
540
+
504
541
  router.post('/webhookFB', async (req, res) => {
505
542
 
506
- winston.verbose("(fbm) Message received from Facebook Instagram");
543
+ winston.verbose("(ig) Webhook received");
507
544
 
508
- let body = req.body;
509
- if (body.object === 'page') {
545
+ if (!verifyWebhookSignature(req)) {
546
+ return res.sendStatus(403);
547
+ }
548
+
549
+ res.status(200).send('EVENT_RECEIVED');
550
+
551
+ setImmediate(async () => {
552
+ try {
553
+ let body = req.body;
554
+ winston.verbose("(ig) Processing webhook asynchronously");
510
555
 
511
556
  let page_id = body.entry[0].id;
512
557
  let PAGE_KEY = "instagram-page-" + page_id;
@@ -595,9 +640,10 @@ router.post('/webhookFB', async (req, res) => {
595
640
  }
596
641
 
597
642
  })
598
- }
599
-
600
- return res.status(200).send('EVENT_RECEIVED');
643
+ } catch (err) {
644
+ winston.error("(ig) Async webhook processing error: ", err?.response?.data || err.message || err);
645
+ }
646
+ });
601
647
  })
602
648
 
603
649
  router.get('/webhookFB', async (req, res) => {
@@ -632,93 +678,63 @@ router.get('/webhookFB', async (req, res) => {
632
678
  return res.sendStatus(403);
633
679
  })
634
680
 
635
- router.get('/oauth', async (req, res) => {
636
- winston.verbose("(fbm) /oauth")
681
+ router.get('/auth/instagram/start', async (req, res) => {
682
+ winston.verbose("(ig) /auth/instagram/start");
637
683
 
638
- let project_id = JSON.parse(req.query.state).project_id;
639
- var code = req.query.code;
640
- let token = JSON.parse(req.query.state).token;
641
- let app_id = JSON.parse(req.query.state).app_id;
642
-
643
- const tdClient = new TiledeskSubscriptionClient({ API_URL: API_URL, project_id: project_id, token: token })
644
- const fbClient = new FacebookClient({ GRAPH_URL: GRAPH_URL, FB_APP_ID: FB_APP_ID, APP_SECRET: APP_SECRET, BASE_URL: BASE_URL });
684
+ let project_id = req.query.project_id;
685
+ let token = req.query.token;
686
+ let app_id = req.query.app_id;
645
687
 
646
- const subscription_info = {
647
- target: BASE_URL + '/tiledesk',
648
- event: 'message.create.request.channel.instagram',
688
+ if (!project_id) {
689
+ return res.status(400).send('Missing project_id parameter');
649
690
  }
650
691
 
651
- let subscription, access_token, pages_list;
652
692
  try {
653
- subscription = await tdClient.subscribe(subscription_info);
654
- access_token = await fbClient.getAccessTokenFromCode(code);
655
- pages_list = await fbClient.getPages(access_token);
693
+ var auth = instagramOAuth.getAuthorizationUrl(project_id, token, app_id);
694
+ winston.info("(ig) Redirecting to Instagram OAuth for project " + project_id);
695
+ res.redirect(auth.url);
656
696
  } catch (err) {
657
- winston.error("(fbm) /oauth error: ", err?.response?.data || err.message || err);
658
- return res.status(500).send("Error during Facebook connection. Please verify the authorization and retry.");
697
+ winston.error("(ig) OAuth start error: ", err.message);
698
+ res.status(500).send('OAuth configuration error: ' + err.message);
659
699
  }
700
+ });
660
701
 
661
- winston.debug("(fbm) subscription: " + subscription);
662
- winston.debug("(fbm) access_token: " + access_token);
663
- winston.debug("(fbm) pages_list: " + pages_list);
664
-
665
- let CONTENT_KEY = "instagram-" + project_id;
666
-
667
- let pages = [];
668
- pages_list.forEach(async (single_page) => {
669
- let page = {
670
- id: single_page.id,
671
- name: single_page.name,
672
- access_token: single_page.access_token,
673
- category: single_page.category,
674
- active: false
675
- }
676
- pages.push(page);
677
- try {
678
- let event_sub = await fbClient.messageEventSubscription(single_page.id, single_page.access_token);
679
- winston.debug("(fbm) event subscription: " + event_sub?.status + " " + event_sub?.statusText)
680
- } catch (err) {
681
- winston.error("(fbm) event subscription error: ", err?.response?.data || err.message || err);
682
- }
683
- })
702
+ router.get('/oauth-callback', async (req, res) => {
703
+ winston.verbose("(ig) /oauth-callback");
684
704
 
685
- let settings = {
686
- project_id: project_id,
687
- token: token,
688
- access_token: access_token,
689
- subscription_id: subscription._id,
690
- secret: subscription.secret,
691
- pages: pages,
692
- app_id: app_id
705
+ var code = req.query.code;
706
+ var stateStr = req.query.state;
707
+ if (!code || !stateStr) {
708
+ winston.error("(ig) Missing code or state parameter");
709
+ return res.status(400).send("Missing authorization code or state");
693
710
  }
694
711
 
695
- await db.set(CONTENT_KEY, settings);
712
+ var state = instagramOAuth.verifyState(stateStr);
713
+ if (!state) {
714
+ winston.error("(ig) Invalid or expired state");
715
+ return res.status(400).send("Invalid or expired state parameter. Please try connecting again.");
716
+ }
696
717
 
697
- //let settings_retrived = await db.get(CONTENT_KEY);
698
- //var redirect_uri = DASHBOARD_BASE_URL + "/#/project/" + project_id + "/app-store-install/" + app_id + "/run";
699
- var redirect_uri = DASHBOARD_BASE_URL + "/#/project/" + project_id + "/integrations?name=instagram";
700
- console.log("(fbm) redirect_uri: ", redirect_uri);
701
- res.redirect(redirect_uri);
718
+ var projectId = state.project_id;
702
719
 
703
- /*
704
- readHTMLFile('/configure.html', (err, html) => {
705
- var template = handlebars.compile(html);
706
- var replacements = {
707
- app_version: pjson.version,
708
- project_id: project_id,
709
- connected: true,
710
- token: token,
711
- app_id: app_id,
712
- endpoint: BASE_URL,
713
- subscription_id: settings.subscription_id,
714
- secret: settings.secret,
715
- pages: settings.pages
716
- }
717
- var html = template(replacements)
718
- res.send(html);
719
- })
720
- */
720
+ try {
721
+ var tokenData = await instagramOAuth.exchangeCodeForToken(code);
722
+ winston.info("(ig) Token obtained for project " + projectId + ", user: " + (tokenData.username || 'unknown'));
723
+
724
+ var CONTENT_KEY = "instagram-" + projectId;
725
+ var settings = await db.get(CONTENT_KEY) || {};
726
+ settings.ig_oauth_token = instagramOAuth.encryptToken(tokenData);
727
+ settings.instagram_username = tokenData.username;
728
+ settings.instagram_business_id = tokenData.instagram_business_id;
729
+ settings.connected = true;
730
+ await db.set(CONTENT_KEY, settings);
721
731
 
732
+ var redirect_uri = DASHBOARD_BASE_URL + "/#/project/" + projectId + "/integrations?name=instagram";
733
+ res.redirect(redirect_uri);
734
+ } catch (err) {
735
+ winston.error("(ig) OAuth callback error: ", err?.response?.data || err.message || err);
736
+ res.status(500).send("Authentication failed: " + (err.message || 'Unknown error'));
737
+ }
722
738
  })
723
739
 
724
740
  router.post('/enablePage', async (req, res) => {
@@ -858,38 +874,25 @@ router.post('/disablePage', async (req, res) => {
858
874
  })
859
875
 
860
876
  router.post('/disconnect', async (req, res) => {
861
- winston.verbose("(fbm) /disconnect");
877
+ winston.verbose("(ig) /disconnect");
862
878
 
863
879
  let project_id = req.body.project_id;
864
880
  let token = req.body.token;
865
- let subscription_id = req.body.subscription_id;
866
881
 
867
882
  let CONTENT_KEY = "instagram-" + project_id;
868
-
869
883
  let settings = await db.get(CONTENT_KEY);
870
884
 
871
- if (settings && settings.pages) {
872
- const active_page = settings.pages.find(p => p.active === true);
873
- if (active_page) {
874
- let PAGE_KEY = "instagram-page-" + active_page.id;
875
- await db.remove(PAGE_KEY)
876
- winston.debug("(fbm) Page deleted.");
877
- }
878
- }
879
-
880
- await db.remove(CONTENT_KEY)
881
- winston.verbose("(fbm) Content deleted.");
882
-
883
- const tdClient = new TiledeskSubscriptionClient({ API_URL: API_URL, project_id: project_id, token: token })
884
- if (settings && settings.subscription_id) {
885
- tdClient.unsubscribe(settings.subscription_id).catch((err) => {
886
- winston.error("(fbm) unsubscribe error: ", err);
887
- });
885
+ if (settings) {
886
+ settings.ig_oauth_token = null;
887
+ settings.instagram_username = null;
888
+ settings.instagram_business_id = null;
889
+ settings.connected = false;
890
+ await db.set(CONTENT_KEY, settings);
891
+ winston.info("(ig) OAuth token cleared for project " + project_id);
888
892
  }
889
893
 
890
894
  const tdChannel = new TiledeskChannel({ settings: { project_id: project_id, token: token }, API_URL: API_URL })
891
895
  let departments = await tdChannel.getDepartments(token).catch(() => []);
892
- winston.debug("(fbm) found " + departments.length + " departments")
893
896
 
894
897
  readHTMLFile('/configure.html', (err, html) => {
895
898
  var template = handlebars.compile(html);
@@ -900,12 +903,12 @@ router.post('/disconnect', async (req, res) => {
900
903
  token: token,
901
904
  endpoint: BASE_URL,
902
905
  departments: departments,
903
- brand_name: BRAND_NAME
906
+ brand_name: BRAND_NAME,
907
+ oauth_url: BASE_URL + '/auth/instagram/start?project_id=' + project_id + '&token=' + token
904
908
  }
905
909
  var html = template(replacements)
906
910
  return res.send(html);
907
911
  })
908
-
909
912
  })
910
913
 
911
914
  function startApp(settings, callback) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@resultcrafter/aimanager-instagram-connector",
3
- "version": "0.1.2",
3
+ "version": "0.2.1",
4
4
  "description": "AI Manager Instagram DM connector",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -247,7 +247,7 @@
247
247
  var height = 700;
248
248
  var left = (screen.width - width) / 2;
249
249
  var top = (screen.height - height) / 2;
250
- var oauthUrl = './auth/instagram/start?project_id={{ project_id }}&token={{ token }}&app_id={{ app_id }}';
250
+ var oauthUrl = '{{ oauth_url }}';
251
251
  var popup = window.open(oauthUrl, 'instagram-oauth', 'width=' + width + ',height=' + height + ',left=' + left + ',top=' + top);
252
252
 
253
253
  var timer = setInterval(function() {