@vida-global/apps-tools 1.0.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.
- package/index.js +23 -0
- package/lib/actionHandlers/appFunctionHandler.js +7 -0
- package/lib/actionHandlers/appHooksHandler.js +7 -0
- package/lib/actionHandlers/index.js +4 -0
- package/lib/appResponses/appFunctionResponse.js +23 -0
- package/lib/appResponses/appHookResponse.js +21 -0
- package/lib/appResponses/calendarAppResponse.js +6 -0
- package/lib/appResponses/index.js +9 -0
- package/lib/apps/appManager/abstractAppManager.js +70 -0
- package/lib/apps/appManager/appServerAppManager.js +237 -0
- package/lib/apps/appManager/vaderApiClient.js +81 -0
- package/lib/apps/appManager/vidaLiveAppManager.js +187 -0
- package/lib/apps/vidaApp.js +345 -0
- package/lib/errors/illegalAppInvocationError.js +8 -0
- package/lib/openApi/baseApiSpec.js +10 -0
- package/lib/openApi/index.js +150 -0
- package/lib/providers/provider.js +141 -0
- package/lib/providers/providerManager.js +73 -0
- package/lib/providers/providers/auth/generic/index.js +60 -0
- package/lib/providers/providers/auth/google/index.js +251 -0
- package/lib/providers/providers/auth/hubspot/index.js +313 -0
- package/lib/providers/providers/auth/mcp/index.js +99 -0
- package/lib/providers/providers/auth/outlook/index.js +257 -0
- package/lib/providers/providers/auth/squareUp/index.js +111 -0
- package/lib/providers/providers/mock.js +51 -0
- package/lib/server/appController.js +112 -0
- package/lib/server/server.js +73 -0
- package/lib/userContext.js +218 -0
- package/package.json +33 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
const { logger } = require('@vida-global/core')
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ProviderManager {
|
|
5
|
+
#isServer;
|
|
6
|
+
#redisClient;
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
constructor(redisClient, isServer) {
|
|
10
|
+
this.#isServer = isServer;
|
|
11
|
+
this.#redisClient = redisClient;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
get providersDirectory() {
|
|
16
|
+
return `${__dirname}/providers`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
providerPath(providerType, providerName) {
|
|
21
|
+
return `${this.providersDirectory}/${providerType}/${providerName}/index.js`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
getProvider(providerType, providerName) {
|
|
26
|
+
const providerPath = this.providerPath(providerType, providerName)
|
|
27
|
+
return require(providerPath);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
async buildProvider(providerType, providerName, userContext, vanillaConfig, appId, appVersion) {
|
|
32
|
+
const providerCls = this.getProvider(providerType, providerName);
|
|
33
|
+
if (!providerCls) {
|
|
34
|
+
throw new Error(`Provider ${providerName} not found`)
|
|
35
|
+
}
|
|
36
|
+
logger.info(`VADER: Building provider ${providerType}:${providerName}`)
|
|
37
|
+
return await providerCls.get(
|
|
38
|
+
providerCls, {
|
|
39
|
+
userContext,
|
|
40
|
+
redisClient: this.#redisClient,
|
|
41
|
+
vanillaConfig: vanillaConfig,
|
|
42
|
+
appId, appVersion,
|
|
43
|
+
isServer: this.#isServer,
|
|
44
|
+
}
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
async buildRoutes(server, providerList) {
|
|
50
|
+
logger.info('VADER: Building provider configurator routes')
|
|
51
|
+
for (const providerTypeName of providerList) {
|
|
52
|
+
const providerType = providerTypeName.split(':')[0]
|
|
53
|
+
const providerName = providerTypeName.split(':')[1]
|
|
54
|
+
const providerInstance = await this.buildProvider(providerType,
|
|
55
|
+
providerName,
|
|
56
|
+
null,
|
|
57
|
+
null,
|
|
58
|
+
null,
|
|
59
|
+
null);
|
|
60
|
+
if (providerInstance.configurator) {
|
|
61
|
+
logger.info(`VADER: Building routes for provider ${providerType}:${providerName}`)
|
|
62
|
+
const configuratorInstance = await providerInstance.getConfigurator()
|
|
63
|
+
configuratorInstance.buildRoutes(server)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
module.exports = {
|
|
71
|
+
ProviderManager
|
|
72
|
+
}
|
|
73
|
+
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const {google} = require('googleapis');
|
|
2
|
+
const {logger} = require('@vida-global/core');
|
|
3
|
+
const {BaseProvider, BaseProviderConfigurator} = require('../../../provider');
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class GenericAuthConfigurator extends BaseProviderConfigurator {
|
|
7
|
+
async initialize() {
|
|
8
|
+
logger.info('Generic Auth Configurator Initialized')
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
class GenericAuthProvider extends BaseProvider {
|
|
13
|
+
type = 'auth'
|
|
14
|
+
name = 'generic'
|
|
15
|
+
configurator = GenericAuthConfigurator
|
|
16
|
+
|
|
17
|
+
async initialize () {
|
|
18
|
+
logger.info('Generic Auth Provider Initialized')
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async uninstall () {
|
|
22
|
+
logger.info('Generic Auth Provider Uninstalled')
|
|
23
|
+
await this.userContext.deleteProviderConfig(
|
|
24
|
+
this.type, this.name, this.appId, this.appVersion
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async update (config) {
|
|
29
|
+
logger.info('Generic Auth Provider Updated')
|
|
30
|
+
await this.userContext.setProviderConfig(
|
|
31
|
+
this.type, this.name, config,
|
|
32
|
+
this.appId, this.appVersion
|
|
33
|
+
)
|
|
34
|
+
this.config = config;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async getManifest () {
|
|
38
|
+
return this.config
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async isConfigured () {
|
|
42
|
+
if (this.config && this.config.parameters) {
|
|
43
|
+
const {properties, required} = this.config.parameters;
|
|
44
|
+
if (required && Array.isArray(required)) {
|
|
45
|
+
for (const field of required) {
|
|
46
|
+
const property = properties[field];
|
|
47
|
+
if (!property || !property.value ||
|
|
48
|
+
(property.type === 'string' && property.value.trim() === '') ||
|
|
49
|
+
(typeof property.value !== property.type)) {
|
|
50
|
+
console.log(`Missing required field: ${field}`, property)
|
|
51
|
+
return false; // Mark as not configured if a required field is missing, has no value, or type mismatch
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return true; // All required fields exist and are properly configured
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
module.exports = GenericAuthProvider;
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
const axios = require('axios');
|
|
2
|
+
const { URL } = require('url');
|
|
3
|
+
const { BaseProvider, BaseProviderConfigurator } = require('../../../provider');
|
|
4
|
+
const { logger } = require('@vida-global/core');
|
|
5
|
+
const _ = require('lodash');
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
const oAuthConfig = {
|
|
9
|
+
scopes: [
|
|
10
|
+
'https://www.googleapis.com/auth/calendar',
|
|
11
|
+
'https://www.googleapis.com/auth/calendar.events',
|
|
12
|
+
'https://www.googleapis.com/auth/userinfo.profile'
|
|
13
|
+
],
|
|
14
|
+
initiateOAuthUrl: '/vader/oauth/google/initiate',
|
|
15
|
+
callbackPath: '/vader/oauth/google/callback',
|
|
16
|
+
authorizationEndpoint: 'https://accounts.google.com/o/oauth2/v2/auth',
|
|
17
|
+
tokenEndpoint: 'https://oauth2.googleapis.com/token'
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class GoogleOAuthConfigurator extends BaseProviderConfigurator {
|
|
22
|
+
async storeToken(user, tokenResponse) {
|
|
23
|
+
let tokenInfo = {
|
|
24
|
+
accessToken: tokenResponse.access_token,
|
|
25
|
+
refreshToken: tokenResponse.refresh_token,
|
|
26
|
+
expiresAt: Date.now() + (tokenResponse.expires_in * 1000)
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
await this.redis.set(this.TOKEN_KEY(user), JSON.stringify(tokenInfo));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
getAuthorizationUrl(redirectUri, state) {
|
|
33
|
+
const params = new URLSearchParams({
|
|
34
|
+
client_id: this.config.clientId,
|
|
35
|
+
response_type: 'code',
|
|
36
|
+
redirect_uri: redirectUri,
|
|
37
|
+
scope: oAuthConfig.scopes.join(' '),
|
|
38
|
+
access_type: 'offline',
|
|
39
|
+
state: state
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
return `${oAuthConfig.authorizationEndpoint}?${params.toString()}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
buildRoutes(app) {
|
|
46
|
+
app.get(oAuthConfig.initiateOAuthUrl, async (req, res) => {
|
|
47
|
+
const {redirectUri, userId} = req.query;
|
|
48
|
+
|
|
49
|
+
if (!redirectUri || !userId) {
|
|
50
|
+
return res.status(400).send('Missing required parameters: redirectUri and userId');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const stateData = {
|
|
54
|
+
redirectUri,
|
|
55
|
+
userId
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const authUrl = this.getAuthorizationUrl(redirectUri, Buffer.from(JSON.stringify(stateData)).toString('base64'));
|
|
59
|
+
res.redirect(authUrl);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
app.get(oAuthConfig.callbackPath, async (req, res) => {
|
|
63
|
+
let redirectUri = null;
|
|
64
|
+
try {
|
|
65
|
+
redirectUri = await this.handleOAuthCallback(req);
|
|
66
|
+
res.redirect(redirectUri);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
console.error('OAuth callback error:', error);
|
|
69
|
+
res.status(500).send('Authentication failed. Please try again.');
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async handleOAuthCallback(req) {
|
|
75
|
+
if (req.query.error) {
|
|
76
|
+
throw new Error(`OAuth error: ${req.query.error}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const {code, state} = req.query;
|
|
80
|
+
if (!code) {
|
|
81
|
+
throw new Error('No authorization code received');
|
|
82
|
+
}
|
|
83
|
+
if (!state) {
|
|
84
|
+
throw new Error('No state parameter received');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const sessionData = JSON.parse(Buffer.from(state, 'base64').toString());
|
|
88
|
+
let {redirectUri, userId} = sessionData;
|
|
89
|
+
if (!redirectUri || !userId) {
|
|
90
|
+
throw new Error('Invalid state parameter');
|
|
91
|
+
}
|
|
92
|
+
redirectUri = decodeURIComponent(redirectUri);
|
|
93
|
+
let user = {id: userId};
|
|
94
|
+
let success = true;
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
const response = await axios.post(
|
|
98
|
+
oAuthConfig.tokenEndpoint,
|
|
99
|
+
new URLSearchParams({
|
|
100
|
+
client_id: this.config.clientId,
|
|
101
|
+
client_secret: this.config.clientSecret,
|
|
102
|
+
code: code,
|
|
103
|
+
redirect_uri: redirectUri,
|
|
104
|
+
grant_type: 'authorization_code'
|
|
105
|
+
}).toString(),
|
|
106
|
+
{
|
|
107
|
+
headers: {
|
|
108
|
+
'Content-Type': 'application/x-www-form-urlencoded'
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
);
|
|
112
|
+
await this.storeToken(user, response.data);
|
|
113
|
+
} catch (error) {
|
|
114
|
+
console.error('Error exchanging code for token:', error);
|
|
115
|
+
success = false;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const finalUrl = new URL(redirectUri);
|
|
119
|
+
finalUrl.searchParams.append('success', success ? 'true' : 'false');
|
|
120
|
+
finalUrl.searchParams.append('userId', user.id);
|
|
121
|
+
return finalUrl.toString();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
class GoogleAuthProvider extends BaseProvider {
|
|
126
|
+
type = 'auth';
|
|
127
|
+
name = 'oauth2';
|
|
128
|
+
configurator = GoogleOAuthConfigurator;
|
|
129
|
+
|
|
130
|
+
TOKEN_KEY(user) {
|
|
131
|
+
return `vader:oauth:google:${user.id}:token`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async initialize() {
|
|
135
|
+
if (!this.isServer && this.user) {
|
|
136
|
+
const tokenData = await this.redis.get(this.TOKEN_KEY(this.user));
|
|
137
|
+
if (tokenData) {
|
|
138
|
+
this.tokenInfo = JSON.parse(tokenData);
|
|
139
|
+
}
|
|
140
|
+
if (this.isTokenExpired()) {
|
|
141
|
+
await this.refreshToken();
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
this.config = {
|
|
145
|
+
...this.config,
|
|
146
|
+
...oAuthConfig
|
|
147
|
+
}
|
|
148
|
+
logger.info('Google OAuth Provider Initialized')
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async uninstall() {
|
|
152
|
+
await this.redis.del(this.TOKEN_KEY(this.user));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async isConfigured() {
|
|
156
|
+
if (!this.config.clientId || !this.config.clientSecret) {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (!this.tokenInfo) {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (this.isTokenExpired() && !(await this.refreshToken())) {
|
|
165
|
+
logger.warn('Google OAuth Provider: Token expired and failed to refresh');
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async getManifest() {
|
|
173
|
+
if (this.isServer) {
|
|
174
|
+
return {
|
|
175
|
+
...oAuthConfig
|
|
176
|
+
}
|
|
177
|
+
} else {
|
|
178
|
+
return {
|
|
179
|
+
...oAuthConfig,
|
|
180
|
+
parameters: {
|
|
181
|
+
type: "object",
|
|
182
|
+
properties: {
|
|
183
|
+
accessToken: {
|
|
184
|
+
type: "string",
|
|
185
|
+
description: "Access Token for Google API.",
|
|
186
|
+
value: await this.getAccessToken(),
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
required: ["accessToken"],
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
isTokenExpired() {
|
|
196
|
+
if (!this.tokenInfo || !this.tokenInfo.expiresAt) {
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
// Add a 5-minute buffer before expiration
|
|
200
|
+
return Date.now() >= (this.tokenInfo.expiresAt - 300000);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async getAccessToken() {
|
|
204
|
+
if (this.isTokenExpired()) {
|
|
205
|
+
await this.refreshToken();
|
|
206
|
+
}
|
|
207
|
+
return this.tokenInfo?.accessToken;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async refreshToken() {
|
|
211
|
+
if (!this.tokenInfo?.refreshToken) {
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
const response = await axios.post(
|
|
217
|
+
oAuthConfig.tokenEndpoint,
|
|
218
|
+
new URLSearchParams({
|
|
219
|
+
client_id: this.config.clientId,
|
|
220
|
+
client_secret: this.config.clientSecret,
|
|
221
|
+
refresh_token: this.tokenInfo.refreshToken,
|
|
222
|
+
grant_type: 'refresh_token'
|
|
223
|
+
}).toString(),
|
|
224
|
+
{
|
|
225
|
+
headers: {
|
|
226
|
+
'Content-Type': 'application/x-www-form-urlencoded'
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
await this.storeToken(response.data);
|
|
232
|
+
return true;
|
|
233
|
+
} catch (error) {
|
|
234
|
+
logger.error('Error refreshing token:', error);
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async storeToken(tokenResponse) {
|
|
240
|
+
let tokenInfo = {
|
|
241
|
+
accessToken: tokenResponse.access_token,
|
|
242
|
+
refreshToken: tokenResponse.refresh_token || this.tokenInfo?.refreshToken,
|
|
243
|
+
expiresAt: Date.now() + (tokenResponse.expires_in * 1000)
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
this.tokenInfo = tokenInfo;
|
|
247
|
+
await this.redis.set(this.TOKEN_KEY(this.user), JSON.stringify(tokenInfo));
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
module.exports = GoogleAuthProvider;
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
const axios = require('axios');
|
|
2
|
+
const { URL } = require('url');
|
|
3
|
+
const { logger } = require('@vida-global/core');
|
|
4
|
+
const { BaseProvider, BaseProviderConfigurator } = require('../../../provider');
|
|
5
|
+
const { VaderUserContext } = require('../../../../userContext');
|
|
6
|
+
const _ = require('lodash');
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
const oAuthConfig = {
|
|
10
|
+
initiateOAuthUrl: '/vader/oauth/hubspot/initiate',
|
|
11
|
+
callbackPath: '/vader/oauth/hubspot/callback',
|
|
12
|
+
authorizationEndpoint: 'https://app.hubspot.com/oauth/authorize',
|
|
13
|
+
tokenEndpoint: 'https://api.hubapi.com/oauth/v1/token',
|
|
14
|
+
scopes: [
|
|
15
|
+
'crm.objects.contacts.read',
|
|
16
|
+
'crm.objects.contacts.write',
|
|
17
|
+
'crm.objects.companies.read',
|
|
18
|
+
'forms',
|
|
19
|
+
'oauth',
|
|
20
|
+
'tickets',
|
|
21
|
+
]
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class HubspotOAuthConfigurator extends BaseProviderConfigurator {
|
|
26
|
+
async initialize() {
|
|
27
|
+
logger.info('HubSpot OAuth Configurator Initialized')
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async storeToken(user, tokenResponse, appId, appVersion) {
|
|
31
|
+
let tokenData = {
|
|
32
|
+
accessToken: tokenResponse.access_token,
|
|
33
|
+
refreshToken: tokenResponse.refresh_token,
|
|
34
|
+
expiresAt: Date.now() + (tokenResponse.expires_in * 1000)
|
|
35
|
+
};
|
|
36
|
+
let ctx = this.userContext
|
|
37
|
+
if (!ctx) {
|
|
38
|
+
// Build a new user context using our own
|
|
39
|
+
ctx = new VaderUserContext(this)
|
|
40
|
+
await ctx.load({ user })
|
|
41
|
+
}
|
|
42
|
+
let providerConfig = await ctx.getProviderConfig(
|
|
43
|
+
this.type, this.name, appId, appVersion
|
|
44
|
+
)
|
|
45
|
+
providerConfig = _.merge(_.cloneDeep(providerConfig), {
|
|
46
|
+
tokenData: tokenData,
|
|
47
|
+
})
|
|
48
|
+
await ctx.setProviderConfig(
|
|
49
|
+
this.type, this.name, providerConfig,
|
|
50
|
+
appId, appVersion
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
getAuthorizationUrl(redirectUri, state) {
|
|
55
|
+
const params = new URLSearchParams({
|
|
56
|
+
client_id: this.globalConfig.clientId,
|
|
57
|
+
redirect_uri: redirectUri,
|
|
58
|
+
scope: oAuthConfig.scopes.join(' '),
|
|
59
|
+
state: state
|
|
60
|
+
});
|
|
61
|
+
return `${oAuthConfig.authorizationEndpoint}?${params.toString()}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
buildRoutes(app) {
|
|
65
|
+
if (this.hasBuiltRoutes) {
|
|
66
|
+
logger.warn('OAuth routes already built');
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
app.get(oAuthConfig.initiateOAuthUrl, async (req, res) => {
|
|
71
|
+
let {redirectUri, finalUri, userId, appId, appVersion} = req.query;
|
|
72
|
+
|
|
73
|
+
// TODO: Remove this after passing from frontend
|
|
74
|
+
if (!appId) {
|
|
75
|
+
appId = 'hubspot';
|
|
76
|
+
}
|
|
77
|
+
if (!appVersion) {
|
|
78
|
+
appVersion = 'v1';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!redirectUri || !finalUri || !userId || !appId || !appVersion) {
|
|
82
|
+
return res.status(400).send(
|
|
83
|
+
'Missing one or more required parameters: redirectUri, finalUri, userId, appId, appVersion'
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const stateData = {
|
|
88
|
+
redirectUri,
|
|
89
|
+
finalUri,
|
|
90
|
+
userId,
|
|
91
|
+
appId,
|
|
92
|
+
appVersion,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const authUrl = this.getAuthorizationUrl(
|
|
96
|
+
redirectUri, Buffer.from(JSON.stringify(stateData)).toString('base64')
|
|
97
|
+
);
|
|
98
|
+
res.redirect(authUrl);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
app.get(oAuthConfig.callbackPath, async (req, res) => {
|
|
102
|
+
let redirectUri = null;
|
|
103
|
+
try {
|
|
104
|
+
redirectUri = await this.handleOAuthCallback(req);
|
|
105
|
+
res.redirect(redirectUri);
|
|
106
|
+
} catch (error) {
|
|
107
|
+
logger.error('OAuth callback error:', error);
|
|
108
|
+
res.status(500).send('Authentication failed. Please try again.');
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
this.hasBuiltRoutes = true;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async handleOAuthCallback(req) {
|
|
115
|
+
if (req.query.error) {
|
|
116
|
+
throw new Error(`OAuth error: ${req.query.error}`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const {code, state} = req.query;
|
|
120
|
+
if (!code) {
|
|
121
|
+
throw new Error('No authorization code received');
|
|
122
|
+
}
|
|
123
|
+
if (!state) {
|
|
124
|
+
throw new Error('No state parameter received');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const sessionData = JSON.parse(Buffer.from(state, 'base64').toString());
|
|
128
|
+
let {redirectUri, finalUri, userId, appId, appVersion} = sessionData;
|
|
129
|
+
|
|
130
|
+
// TODO: Remove this after passing from frontend
|
|
131
|
+
if (!appId) {
|
|
132
|
+
appId = 'hubspot';
|
|
133
|
+
}
|
|
134
|
+
if (!appVersion) {
|
|
135
|
+
appVersion = 'v1';
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (!redirectUri || !finalUri || !userId || !appId || !appVersion) {
|
|
139
|
+
throw new Error('Invalid state parameters');
|
|
140
|
+
}
|
|
141
|
+
redirectUri = decodeURIComponent(redirectUri);
|
|
142
|
+
let user = {id: userId};
|
|
143
|
+
let success = true;
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
const response = await axios.post(
|
|
147
|
+
oAuthConfig.tokenEndpoint,
|
|
148
|
+
new URLSearchParams({
|
|
149
|
+
grant_type: 'authorization_code',
|
|
150
|
+
client_id: this.globalConfig.clientId,
|
|
151
|
+
client_secret: this.globalConfig.clientSecret,
|
|
152
|
+
redirect_uri: redirectUri,
|
|
153
|
+
code: code
|
|
154
|
+
}).toString(),
|
|
155
|
+
{
|
|
156
|
+
headers: {
|
|
157
|
+
'Content-Type': 'application/x-www-form-urlencoded'
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
);
|
|
161
|
+
await this.storeToken(user, response.data, appId, appVersion);
|
|
162
|
+
} catch (error) {
|
|
163
|
+
logger.error('Error exchanging code for token:');
|
|
164
|
+
console.error(error)
|
|
165
|
+
success = false;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const finalUrl = new URL(finalUri);
|
|
169
|
+
finalUrl.searchParams.set('hubspotOAuthSuccess', success ? 'true' : 'false');
|
|
170
|
+
finalUrl.searchParams.set('userId', user.id);
|
|
171
|
+
return finalUrl.toString();
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class HubspotAuthProvider extends BaseProvider {
|
|
177
|
+
type = 'auth';
|
|
178
|
+
name = 'hubspot';
|
|
179
|
+
configurator = HubspotOAuthConfigurator;
|
|
180
|
+
|
|
181
|
+
async initialize() {
|
|
182
|
+
if (!this.isServer && this.userContext) {
|
|
183
|
+
const providerConfig = await this.userContext.getProviderConfig(
|
|
184
|
+
this.type, this.name, this.appId, this.appVersion
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
this.config = {
|
|
188
|
+
...this.config,
|
|
189
|
+
...providerConfig
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (this.isTokenExpired()) {
|
|
193
|
+
await this.refreshToken();
|
|
194
|
+
}
|
|
195
|
+
this.config.parameters = {
|
|
196
|
+
type: "object",
|
|
197
|
+
properties: {
|
|
198
|
+
accessToken: {
|
|
199
|
+
type: "string",
|
|
200
|
+
description: "Access Token to access the Hubspot API.",
|
|
201
|
+
value: await this.getAccessToken(),
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
required: ["accessToken"],
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
logger.info('HubSpot OAuth Provider Initialized', this.config)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async uninstall() {
|
|
211
|
+
logger.info('Uninstalling HubSpot OAuth Provider')
|
|
212
|
+
await this.userContext.deleteProviderConfig(
|
|
213
|
+
this.type, this.name, this.appId, this.appVersion
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async update(config) {
|
|
218
|
+
logger.info('Updating HubSpot OAuth Provider')
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async isConfigured() {
|
|
222
|
+
if (!this.globalConfig.clientId || !this.globalConfig.clientSecret) {
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (!this.config.tokenData) {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (this.isTokenExpired() && !(await this.refreshToken())) {
|
|
231
|
+
logger.warn('HubSpot OAuth Provider: Token expired and failed to refresh');
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async getManifest() {
|
|
239
|
+
if (this.isServer) {
|
|
240
|
+
return {
|
|
241
|
+
...oAuthConfig
|
|
242
|
+
}
|
|
243
|
+
} else {
|
|
244
|
+
let manifest = {
|
|
245
|
+
...oAuthConfig,
|
|
246
|
+
...this.config,
|
|
247
|
+
connected: !!await this.getAccessToken()
|
|
248
|
+
}
|
|
249
|
+
return _.omit(manifest, ['clientId', 'clientSecret', 'appId']);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
isTokenExpired() {
|
|
254
|
+
if (!this.config.tokenData || !this.config.tokenData.expiresAt) {
|
|
255
|
+
return true;
|
|
256
|
+
}
|
|
257
|
+
// Add a 5-minute buffer before expiration
|
|
258
|
+
return Date.now() >= (this.config.tokenData.expiresAt - 300000);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async getAccessToken() {
|
|
262
|
+
if (this.isTokenExpired()) {
|
|
263
|
+
await this.refreshToken();
|
|
264
|
+
}
|
|
265
|
+
return this.config.tokenData?.accessToken;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async refreshToken() {
|
|
269
|
+
if (!this.config.tokenData?.refreshToken) {
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
try {
|
|
274
|
+
const response = await axios.post(
|
|
275
|
+
oAuthConfig.tokenEndpoint,
|
|
276
|
+
new URLSearchParams({
|
|
277
|
+
grant_type: 'refresh_token',
|
|
278
|
+
client_id: this.globalConfig.clientId,
|
|
279
|
+
client_secret: this.globalConfig.clientSecret,
|
|
280
|
+
refresh_token: this.config.tokenData.refreshToken
|
|
281
|
+
}).toString(),
|
|
282
|
+
{
|
|
283
|
+
headers: {
|
|
284
|
+
'Content-Type': 'application/x-www-form-urlencoded'
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
await this.storeToken(response.data);
|
|
290
|
+
return true;
|
|
291
|
+
} catch (error) {
|
|
292
|
+
logger.error('Error refreshing token:', error);
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
async storeToken(tokenResponse) {
|
|
298
|
+
let tokenData = {
|
|
299
|
+
accessToken: tokenResponse.access_token,
|
|
300
|
+
refreshToken: tokenResponse.refresh_token || this.config.tokenData?.refreshToken,
|
|
301
|
+
expiresAt: Date.now() + (tokenResponse.expires_in * 1000)
|
|
302
|
+
};
|
|
303
|
+
this.config = _.merge(_.cloneDeep(this.config), {
|
|
304
|
+
tokenData: tokenData,
|
|
305
|
+
})
|
|
306
|
+
await this.userContext.setProviderConfig(
|
|
307
|
+
this.type, this.name, this.config,
|
|
308
|
+
this.appId, this.appVersion
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
module.exports = HubspotAuthProvider;
|