@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.
@@ -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;