From bd7c56c84099a9a2601e74521863aacf89047462 Mon Sep 17 00:00:00 2001 From: Khaliq Date: Wed, 3 Apr 2024 13:52:57 +0300 Subject: [PATCH 01/13] [nan-680] add plan types and admin only api to edit --- .../lib/controllers/access.middleware.ts | 21 ++++++++++ .../lib/controllers/account.controller.ts | 38 ++++++++++++++++++- .../server/lib/controllers/flow.controller.ts | 9 ----- packages/server/lib/server.ts | 7 +++- ...20240403100610_add_account_status_info.cjs | 15 ++++++++ packages/shared/lib/models/Admin.ts | 7 ++++ .../shared/lib/services/account.service.ts | 12 ++++++ 7 files changed, 97 insertions(+), 12 deletions(-) create mode 100644 packages/shared/lib/db/migrations/20240403100610_add_account_status_info.cjs diff --git a/packages/server/lib/controllers/access.middleware.ts b/packages/server/lib/controllers/access.middleware.ts index 2cec069574a..101b112146e 100644 --- a/packages/server/lib/controllers/access.middleware.ts +++ b/packages/server/lib/controllers/access.middleware.ts @@ -5,6 +5,8 @@ import { LogActionEnum, ErrorSourceEnum, environmentService, + accountService, + getEnvironmentAndAccountId, setAccount, setEnvironmentId, errorManager, @@ -13,6 +15,7 @@ import { telemetry, MetricTypes } from '@nangohq/shared'; +import { NANGO_ADMIN_UUID } from './account.controller.js'; import tracer from 'dd-trace'; const logger = getLogger('AccessMiddleware'); @@ -59,6 +62,24 @@ export class AccessMiddleware { next(); } + async adminKeyAuth(req: Request, res: Response, next: NextFunction) { + const { success, error, response } = await getEnvironmentAndAccountId(res, req); + + if (!success || response === null) { + errorManager.errResFromNangoErr(res, error); + return; + } + + const { accountId } = response; + const fullAccount = await accountService.getAccountById(accountId); + + if (fullAccount?.uuid !== NANGO_ADMIN_UUID) { + res.status(401).send('Unauthorized'); + return; + } + next(); + } + async publicKeyAuth(req: Request, res: Response, next: NextFunction) { const publicKey = req.query['public_key'] as string; diff --git a/packages/server/lib/controllers/account.controller.ts b/packages/server/lib/controllers/account.controller.ts index f0e83840b5d..d8fdbbe5407 100644 --- a/packages/server/lib/controllers/account.controller.ts +++ b/packages/server/lib/controllers/account.controller.ts @@ -1,7 +1,15 @@ import type { Request, Response, NextFunction } from 'express'; import type { LogLevel } from '@nangohq/shared'; import { isCloud } from '@nangohq/utils/dist/environment/detection.js'; -import { accountService, userService, errorManager, LogActionEnum, createActivityLogAndLogMessage } from '@nangohq/shared'; +import { + SubscriptionTypes, + getEnvironmentAndAccountId, + accountService, + userService, + errorManager, + LogActionEnum, + createActivityLogAndLogMessage +} from '@nangohq/shared'; import { getUserAccountAndEnvironmentFromSession } from '../utils/utils.js'; export const NANGO_ADMIN_UUID = process.env['NANGO_ADMIN_UUID']; @@ -63,6 +71,34 @@ class AccountController { } } + async editCustomer(req: Request, res: Response, next: NextFunction) { + try { + const { success, error, response } = await getEnvironmentAndAccountId(res, req); + if (!success || response === null) { + errorManager.errResFromNangoErr(res, error); + return; + } + const { accountId } = response; + + const { is_paying, subscription_type } = req.body; + + if (is_paying === undefined || subscription_type === undefined) { + res.status(400).send({ error: 'is_paying and subscription_type are required.' }); + return; + } + + if (!Object.values(SubscriptionTypes).includes(subscription_type as SubscriptionTypes)) { + res.status(400).send({ error: `Invalid subscription type. Valid types are ${Object.values(SubscriptionTypes).join(', ')}` }); + return; + } + + await accountService.editCustomer(is_paying, subscription_type, accountId); + res.status(200).send({ is_paying, subscription_type }); + } catch (err) { + next(err); + } + } + async switchAccount(req: Request, res: Response, next: NextFunction) { if (!AUTH_ADMIN_SWITCH_ENABLED) { res.status(400).send('Account switching only allowed in cloud'); diff --git a/packages/server/lib/controllers/flow.controller.ts b/packages/server/lib/controllers/flow.controller.ts index cf668ae7751..a5a75433737 100644 --- a/packages/server/lib/controllers/flow.controller.ts +++ b/packages/server/lib/controllers/flow.controller.ts @@ -16,7 +16,6 @@ import { getConfigWithEndpointsByProviderConfigKeyAndName, getSyncsByConnectionIdsAndEnvironmentIdAndSyncName } from '@nangohq/shared'; -import { NANGO_ADMIN_UUID } from './account.controller.js'; class FlowController { public async getFlows(req: Request, res: Response, next: NextFunction) { @@ -45,14 +44,6 @@ class FlowController { return; } - const { accountId } = response; - const fullAccount = await accountService.getAccountById(accountId); - - if (fullAccount?.uuid !== NANGO_ADMIN_UUID) { - res.status(401).send('Unauthorized'); - return; - } - const { targetAccountUUID, targetEnvironment, config } = req.body; const result = await accountService.getAccountAndEnvironmentIdByUUID(targetAccountUUID, targetEnvironment); diff --git a/packages/server/lib/server.ts b/packages/server/lib/server.ts index 3cb20285849..c10fca387ed 100644 --- a/packages/server/lib/server.ts +++ b/packages/server/lib/server.ts @@ -50,6 +50,7 @@ const app = express(); setupAuth(app); const apiAuth = [authMiddleware.secretKeyAuth.bind(authMiddleware), rateLimiterMiddleware]; +const adminAuth = [authMiddleware.secretKeyAuth.bind(authMiddleware), authMiddleware.adminKeyAuth.bind(authMiddleware), rateLimiterMiddleware]; const apiPublicAuth = [authMiddleware.publicKeyAuth.bind(authMiddleware), rateLimiterMiddleware]; const webAuth = isCloud || isEnterprise @@ -99,6 +100,10 @@ app.route('/api-auth/basic/:providerConfigKey').post(apiPublicAuth, apiAuthContr app.route('/app-store-auth/:providerConfigKey').post(apiPublicAuth, appStoreAuthController.auth.bind(appStoreAuthController)); app.route('/unauth/:providerConfigKey').post(apiPublicAuth, unAuthController.create.bind(unAuthController)); +// API Admin routes +app.route('/admin/flow/deploy/pre-built').post(adminAuth, flowController.adminDeployPrivateFlow.bind(flowController)); +app.route('/admin/customer').patch(adminAuth, accountController.editCustomer.bind(accountController)); + // API routes (API key auth). app.route('/provider').get(apiAuth, providerController.listProviders.bind(providerController)); app.route('/provider/:provider').get(apiAuth, providerController.getProvider.bind(providerController)); @@ -132,8 +137,6 @@ app.route('/action/trigger').post(apiAuth, syncController.triggerAction.bind(syn app.route('/v1/*').all(apiAuth, syncController.actionOrModel.bind(syncController)); -app.route('/admin/flow/deploy/pre-built').post(apiAuth, flowController.adminDeployPrivateFlow.bind(flowController)); - app.route('/proxy/*').all(apiAuth, upload.any(), proxyController.routeCall.bind(proxyController)); // Webapp routes (no auth). diff --git a/packages/shared/lib/db/migrations/20240403100610_add_account_status_info.cjs b/packages/shared/lib/db/migrations/20240403100610_add_account_status_info.cjs new file mode 100644 index 00000000000..4d77266de02 --- /dev/null +++ b/packages/shared/lib/db/migrations/20240403100610_add_account_status_info.cjs @@ -0,0 +1,15 @@ +const tableName = '_nango_accounts'; + +exports.up = async function (knex, _) { + await knex.schema.alterTable(tableName, function (table) { + table.boolean('is_paying').defaultTo(false); + table.string('subscription_type'); + }); +}; + +exports.down = async function (knex, _) { + await knex.schema.table(tableName, function (table) { + table.dropColumn('is_paying'); + table.dropColumn('subscription_type'); + }); +}; diff --git a/packages/shared/lib/models/Admin.ts b/packages/shared/lib/models/Admin.ts index ebd854086d6..44e7f85ebb9 100644 --- a/packages/shared/lib/models/Admin.ts +++ b/packages/shared/lib/models/Admin.ts @@ -1,5 +1,10 @@ import type { Timestamps } from './Generic.js'; +export enum SubscriptionTypes { + STARTER = 'starter', + SCALE = 'scale' +} + export interface Account { id: number; name: string; @@ -8,6 +13,8 @@ export interface Account { websockets_path?: string; uuid?: string; is_admin?: boolean; + is_paying?: boolean; + subscription_type?: string; } export interface User extends Timestamps { diff --git a/packages/shared/lib/services/account.service.ts b/packages/shared/lib/services/account.service.ts index e2bc3a371eb..e4af04ce945 100644 --- a/packages/shared/lib/services/account.service.ts +++ b/packages/shared/lib/services/account.service.ts @@ -96,6 +96,18 @@ class AccountService { return null; } + + async editCustomer(is_paying: boolean, subscription_type: string, accountId: number): Promise { + try { + await db.knex.update({ is_paying, subscription_type }).from(`_nango_accounts`).where({ id: accountId }); + } catch (e) { + errorManager.report(e, { + source: ErrorSourceEnum.PLATFORM, + operation: LogActionEnum.DATABASE, + accountId + }); + } + } } export default new AccountService(); From c78711e50356528c322eab0710fab76e39a5ca92 Mon Sep 17 00:00:00 2001 From: Khaliq Date: Wed, 3 Apr 2024 13:54:47 +0300 Subject: [PATCH 02/13] [nan-680] allow null --- packages/server/lib/controllers/account.controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/lib/controllers/account.controller.ts b/packages/server/lib/controllers/account.controller.ts index d8fdbbe5407..41320c64924 100644 --- a/packages/server/lib/controllers/account.controller.ts +++ b/packages/server/lib/controllers/account.controller.ts @@ -87,7 +87,7 @@ class AccountController { return; } - if (!Object.values(SubscriptionTypes).includes(subscription_type as SubscriptionTypes)) { + if (subscription_type !== null && !Object.values(SubscriptionTypes).includes(subscription_type as SubscriptionTypes)) { res.status(400).send({ error: `Invalid subscription type. Valid types are ${Object.values(SubscriptionTypes).join(', ')}` }); return; } From f6e4d07274767c02d57d3349d915b59268d2a2e6 Mon Sep 17 00:00:00 2001 From: Khaliq Date: Wed, 3 Apr 2024 14:18:57 +0300 Subject: [PATCH 03/13] [nan-677] start creation hook --- .../lib/controllers/oauth.controller.ts | 8 ++++++++ packages/shared/lib/hooks/hooks.ts | 19 +++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/packages/server/lib/controllers/oauth.controller.ts b/packages/server/lib/controllers/oauth.controller.ts index 931e0becb50..0fcb999fb8f 100644 --- a/packages/server/lib/controllers/oauth.controller.ts +++ b/packages/server/lib/controllers/oauth.controller.ts @@ -21,6 +21,7 @@ import type { } from '@nangohq/shared'; import { getConnectionConfig, + connectionCreationStart as connectionCreationStartHook, connectionCreated as connectionCreatedHook, connectionCreationFailed as connectionCreationFailedHook, interpolateStringFromObject, @@ -92,6 +93,13 @@ class OAuthController { connectionId: String(connectionId) }); + await connectionCreationStartHook({ + providerConfigKey, + connectionId, + environmentId, + activityLogId + }); + const callbackUrl = await getOauthCallbackUrl(environmentId); const connectionConfig = req.query['params'] != null ? getConnectionConfig(req.query['params']) : {}; const authorizationParams = req.query['authorization_params'] != null ? getAdditionalAuthorizationParams(req.query['authorization_params']) : {}; diff --git a/packages/shared/lib/hooks/hooks.ts b/packages/shared/lib/hooks/hooks.ts index 7955bb77cd0..4db9d5faf89 100644 --- a/packages/shared/lib/hooks/hooks.ts +++ b/packages/shared/lib/hooks/hooks.ts @@ -15,6 +15,25 @@ import type { Result } from '../utils/result.js'; import { resultOk, resultErr } from '../utils/result.js'; import { NangoError } from '../utils/error.js'; +export const connectionCreationStart = async ({ + providerConfigKey, + connectionId, + environmentId, + activityLogId +}: { + providerConfigKey: string | undefined; + connectionId: string | undefined; + environmentId: number; + activityLogId: number | null; +}): Promise => { + if (!providerConfigKey || !connectionId || !activityLogId) { + return; + } + + console.log('connectionCreationStart', providerConfigKey, connectionId, environmentId, activityLogId); + return Promise.resolve(); +}; + export const connectionCreated = async ( connection: RecentlyCreatedConnection, provider: string, From d873fc05dc6b66c0ea0ce453dec3d6fa853831ce Mon Sep 17 00:00:00 2001 From: Khaliq Date: Wed, 3 Apr 2024 14:26:53 +0300 Subject: [PATCH 04/13] [nan-680] is capped --- .../lib/controllers/account.controller.ts | 33 +++++++++---------- ...20240403100610_add_account_status_info.cjs | 6 ++-- packages/shared/lib/models/Admin.ts | 8 +---- .../shared/lib/services/account.service.ts | 18 ++++++++-- 4 files changed, 34 insertions(+), 31 deletions(-) diff --git a/packages/server/lib/controllers/account.controller.ts b/packages/server/lib/controllers/account.controller.ts index 41320c64924..069928fc6e9 100644 --- a/packages/server/lib/controllers/account.controller.ts +++ b/packages/server/lib/controllers/account.controller.ts @@ -1,15 +1,7 @@ import type { Request, Response, NextFunction } from 'express'; import type { LogLevel } from '@nangohq/shared'; import { isCloud } from '@nangohq/utils/dist/environment/detection.js'; -import { - SubscriptionTypes, - getEnvironmentAndAccountId, - accountService, - userService, - errorManager, - LogActionEnum, - createActivityLogAndLogMessage -} from '@nangohq/shared'; +import { getEnvironmentAndAccountId, accountService, userService, errorManager, LogActionEnum, createActivityLogAndLogMessage } from '@nangohq/shared'; import { getUserAccountAndEnvironmentFromSession } from '../utils/utils.js'; export const NANGO_ADMIN_UUID = process.env['NANGO_ADMIN_UUID']; @@ -73,27 +65,32 @@ class AccountController { async editCustomer(req: Request, res: Response, next: NextFunction) { try { - const { success, error, response } = await getEnvironmentAndAccountId(res, req); + const { success, error } = await getEnvironmentAndAccountId(res, req); if (!success || response === null) { errorManager.errResFromNangoErr(res, error); return; } - const { accountId } = response; + const { is_capped, accountUUID } = req.body; - const { is_paying, subscription_type } = req.body; + if (!accountUUID) { + res.status(400).send({ error: 'account_uuid property is required' }); + return; + } - if (is_paying === undefined || subscription_type === undefined) { - res.status(400).send({ error: 'is_paying and subscription_type are required.' }); + if (is_capped === undefined || is_capped === null) { + res.status(400).send({ error: 'is_capped property is required' }); return; } - if (subscription_type !== null && !Object.values(SubscriptionTypes).includes(subscription_type as SubscriptionTypes)) { - res.status(400).send({ error: `Invalid subscription type. Valid types are ${Object.values(SubscriptionTypes).join(', ')}` }); + const account = await accountService.getAccountByUUID(accountUUID); + + if (!account) { + res.status(400).send({ error: 'Account not found' }); return; } - await accountService.editCustomer(is_paying, subscription_type, accountId); - res.status(200).send({ is_paying, subscription_type }); + await accountService.editCustomer(is_capped, account.id); + res.status(200).send({ is_capped, accountUUID }); } catch (err) { next(err); } diff --git a/packages/shared/lib/db/migrations/20240403100610_add_account_status_info.cjs b/packages/shared/lib/db/migrations/20240403100610_add_account_status_info.cjs index 4d77266de02..5743dd57a4f 100644 --- a/packages/shared/lib/db/migrations/20240403100610_add_account_status_info.cjs +++ b/packages/shared/lib/db/migrations/20240403100610_add_account_status_info.cjs @@ -2,14 +2,12 @@ const tableName = '_nango_accounts'; exports.up = async function (knex, _) { await knex.schema.alterTable(tableName, function (table) { - table.boolean('is_paying').defaultTo(false); - table.string('subscription_type'); + table.boolean('is_capped').defaultTo(true); }); }; exports.down = async function (knex, _) { await knex.schema.table(tableName, function (table) { - table.dropColumn('is_paying'); - table.dropColumn('subscription_type'); + table.dropColumn('is_capped'); }); }; diff --git a/packages/shared/lib/models/Admin.ts b/packages/shared/lib/models/Admin.ts index 44e7f85ebb9..7ea29a0b260 100644 --- a/packages/shared/lib/models/Admin.ts +++ b/packages/shared/lib/models/Admin.ts @@ -1,10 +1,5 @@ import type { Timestamps } from './Generic.js'; -export enum SubscriptionTypes { - STARTER = 'starter', - SCALE = 'scale' -} - export interface Account { id: number; name: string; @@ -13,8 +8,7 @@ export interface Account { websockets_path?: string; uuid?: string; is_admin?: boolean; - is_paying?: boolean; - subscription_type?: string; + is_capped?: boolean; } export interface User extends Timestamps { diff --git a/packages/shared/lib/services/account.service.ts b/packages/shared/lib/services/account.service.ts index e4af04ce945..efcb444ff16 100644 --- a/packages/shared/lib/services/account.service.ts +++ b/packages/shared/lib/services/account.service.ts @@ -33,6 +33,20 @@ class AccountService { } } + async getAccountByUUID(uuid: string): Promise { + try { + const result = await db.knex.select('*').from(`_nango_accounts`).where({ uuid }).first(); + return result || null; + } catch (e) { + errorManager.report(e, { + source: ErrorSourceEnum.PLATFORM, + operation: LogActionEnum.DATABASE + }); + + return null; + } + } + async getAccountAndEnvironmentIdByUUID(targetAccountUUID: string, targetEnvironment: string): Promise<{ accountId: number; environmentId: number } | null> { const account = await db.knex.select('id').from(`_nango_accounts`).where({ uuid: targetAccountUUID }); @@ -97,9 +111,9 @@ class AccountService { return null; } - async editCustomer(is_paying: boolean, subscription_type: string, accountId: number): Promise { + async editCustomer(is_capped: boolean, accountId: number): Promise { try { - await db.knex.update({ is_paying, subscription_type }).from(`_nango_accounts`).where({ id: accountId }); + await db.knex.update({ is_capped }).from(`_nango_accounts`).where({ id: accountId }); } catch (e) { errorManager.report(e, { source: ErrorSourceEnum.PLATFORM, From 225396437baecacadeaaa53d2a8f1a3792f8dc35 Mon Sep 17 00:00:00 2001 From: Khaliq Date: Wed, 3 Apr 2024 14:27:49 +0300 Subject: [PATCH 05/13] [nan-680] remove unused --- packages/server/lib/controllers/account.controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/lib/controllers/account.controller.ts b/packages/server/lib/controllers/account.controller.ts index 069928fc6e9..0fce7759362 100644 --- a/packages/server/lib/controllers/account.controller.ts +++ b/packages/server/lib/controllers/account.controller.ts @@ -66,7 +66,7 @@ class AccountController { async editCustomer(req: Request, res: Response, next: NextFunction) { try { const { success, error } = await getEnvironmentAndAccountId(res, req); - if (!success || response === null) { + if (!success) { errorManager.errResFromNangoErr(res, error); return; } From 1118e3812ba8a4079e06a0cad5d0bf612bb2a447 Mon Sep 17 00:00:00 2001 From: Khaliq Date: Wed, 3 Apr 2024 14:29:05 +0300 Subject: [PATCH 06/13] [nan-680] proper reference --- packages/server/lib/controllers/account.controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/lib/controllers/account.controller.ts b/packages/server/lib/controllers/account.controller.ts index 0fce7759362..3079cea20df 100644 --- a/packages/server/lib/controllers/account.controller.ts +++ b/packages/server/lib/controllers/account.controller.ts @@ -70,7 +70,7 @@ class AccountController { errorManager.errResFromNangoErr(res, error); return; } - const { is_capped, accountUUID } = req.body; + const { is_capped, account_uuid: accountUUID } = req.body; if (!accountUUID) { res.status(400).send({ error: 'account_uuid property is required' }); From 1bfda39919a2a56fb46095102f463c5510c19b4e Mon Sep 17 00:00:00 2001 From: Khaliq Date: Wed, 3 Apr 2024 19:31:24 +0300 Subject: [PATCH 07/13] [nan-677] start logic for checking the connections --- .../lib/controllers/oauth.controller.ts | 8 --- .../access.middleware.ts | 2 +- .../ratelimit.middleware.ts | 0 .../middleware/resource-capping.middleware.ts | 35 +++++++++++++ packages/server/lib/server.ts | 10 ++-- packages/shared/lib/hooks/hooks.ts | 19 ------- .../services/sync/config/config.service.ts | 49 +++++++++++++++++++ 7 files changed, 91 insertions(+), 32 deletions(-) rename packages/server/lib/{controllers => middleware}/access.middleware.ts (98%) rename packages/server/lib/{controllers => middleware}/ratelimit.middleware.ts (100%) create mode 100644 packages/server/lib/middleware/resource-capping.middleware.ts diff --git a/packages/server/lib/controllers/oauth.controller.ts b/packages/server/lib/controllers/oauth.controller.ts index 0fcb999fb8f..931e0becb50 100644 --- a/packages/server/lib/controllers/oauth.controller.ts +++ b/packages/server/lib/controllers/oauth.controller.ts @@ -21,7 +21,6 @@ import type { } from '@nangohq/shared'; import { getConnectionConfig, - connectionCreationStart as connectionCreationStartHook, connectionCreated as connectionCreatedHook, connectionCreationFailed as connectionCreationFailedHook, interpolateStringFromObject, @@ -93,13 +92,6 @@ class OAuthController { connectionId: String(connectionId) }); - await connectionCreationStartHook({ - providerConfigKey, - connectionId, - environmentId, - activityLogId - }); - const callbackUrl = await getOauthCallbackUrl(environmentId); const connectionConfig = req.query['params'] != null ? getConnectionConfig(req.query['params']) : {}; const authorizationParams = req.query['authorization_params'] != null ? getAdditionalAuthorizationParams(req.query['authorization_params']) : {}; diff --git a/packages/server/lib/controllers/access.middleware.ts b/packages/server/lib/middleware/access.middleware.ts similarity index 98% rename from packages/server/lib/controllers/access.middleware.ts rename to packages/server/lib/middleware/access.middleware.ts index 101b112146e..3e611c39bf2 100644 --- a/packages/server/lib/controllers/access.middleware.ts +++ b/packages/server/lib/middleware/access.middleware.ts @@ -15,7 +15,7 @@ import { telemetry, MetricTypes } from '@nangohq/shared'; -import { NANGO_ADMIN_UUID } from './account.controller.js'; +import { NANGO_ADMIN_UUID } from '../controllers/account.controller.js'; import tracer from 'dd-trace'; const logger = getLogger('AccessMiddleware'); diff --git a/packages/server/lib/controllers/ratelimit.middleware.ts b/packages/server/lib/middleware/ratelimit.middleware.ts similarity index 100% rename from packages/server/lib/controllers/ratelimit.middleware.ts rename to packages/server/lib/middleware/ratelimit.middleware.ts diff --git a/packages/server/lib/middleware/resource-capping.middleware.ts b/packages/server/lib/middleware/resource-capping.middleware.ts new file mode 100644 index 00000000000..eff5ef1d16a --- /dev/null +++ b/packages/server/lib/middleware/resource-capping.middleware.ts @@ -0,0 +1,35 @@ +import type { Request, Response, NextFunction } from 'express'; +import { getLogger } from '@nangohq/utils/dist/logger.js'; +import { getSyncConfigsWithConnectionsByEnvironmentIdAndProviderConfigKey, accountService, getEnvironmentAndAccountId, errorManager } from '@nangohq/shared'; + +const logger = getLogger('Resource Capping Middleware'); + +export const authCheck = async (req: Request, res: Response, next: NextFunction) => { + const { success, error, response } = await getEnvironmentAndAccountId(res, req); + + if (!success || response === null) { + errorManager.errResFromNangoErr(res, error); + return; + } + + const { accountId, environmentId } = response; + + const account = await accountService.getAccountById(accountId); + + if (!account) { + errorManager.errRes(res, 'unknown_account'); + return; + } + + const { providerConfigKey } = req.params; + + if (account.is_capped && providerConfigKey) { + logger.info('is capped'); + // account cannot have more than 3 connections for integrations with active scripts + console.log(providerConfigKey); + const syncConfigs = await getSyncConfigsWithConnectionsByEnvironmentIdAndProviderConfigKey(providerConfigKey, environmentId); + console.log(syncConfigs); + } + + next(); +}; diff --git a/packages/server/lib/server.ts b/packages/server/lib/server.ts index c10fca387ed..a2d8be92604 100644 --- a/packages/server/lib/server.ts +++ b/packages/server/lib/server.ts @@ -9,7 +9,7 @@ import connectionController from './controllers/connection.controller.js'; import authController from './controllers/auth.controller.js'; import unAuthController from './controllers/unauth.controller.js'; import appStoreAuthController from './controllers/appStoreAuth.controller.js'; -import authMiddleware from './controllers/access.middleware.js'; +import authMiddleware from './middleware/access.middleware.js'; import userController from './controllers/user.controller.js'; import proxyController from './controllers/proxy.controller.js'; import activityController from './controllers/activity.controller.js'; @@ -19,7 +19,8 @@ import apiAuthController from './controllers/apiAuth.controller.js'; import appAuthController from './controllers/appAuth.controller.js'; import onboardingController from './controllers/onboarding.controller.js'; import webhookController from './controllers/webhook.controller.js'; -import { rateLimiterMiddleware } from './controllers/ratelimit.middleware.js'; +import { rateLimiterMiddleware } from './middleware/ratelimit.middleware.js'; +import { authCheck } from './middleware/resource-capping.middleware.js'; import path from 'path'; import { dirname } from './utils/utils.js'; import type { WebSocket } from 'ws'; @@ -51,7 +52,7 @@ setupAuth(app); const apiAuth = [authMiddleware.secretKeyAuth.bind(authMiddleware), rateLimiterMiddleware]; const adminAuth = [authMiddleware.secretKeyAuth.bind(authMiddleware), authMiddleware.adminKeyAuth.bind(authMiddleware), rateLimiterMiddleware]; -const apiPublicAuth = [authMiddleware.publicKeyAuth.bind(authMiddleware), rateLimiterMiddleware]; +const apiPublicAuth = [authMiddleware.publicKeyAuth.bind(authMiddleware), authCheck, rateLimiterMiddleware]; const webAuth = isCloud || isEnterprise ? [passport.authenticate('session'), authMiddleware.sessionAuth.bind(authMiddleware), rateLimiterMiddleware] @@ -90,11 +91,12 @@ await oAuthSessionService.clearStaleSessions(); app.get('/health', (_, res) => { res.status(200).send({ result: 'ok' }); }); + app.route('/oauth/callback').get(oauthController.oauthCallback.bind(oauthController)); +app.route('/webhook/:environmentUuid/:providerConfigKey').post(webhookController.receive.bind(proxyController)); app.route('/app-auth/connect').get(appAuthController.connect.bind(appAuthController)); app.route('/oauth/connect/:providerConfigKey').get(apiPublicAuth, oauthController.oauthRequest.bind(oauthController)); app.route('/oauth2/auth/:providerConfigKey').post(apiPublicAuth, oauthController.oauth2RequestCC.bind(oauthController)); -app.route('/webhook/:environmentUuid/:providerConfigKey').post(webhookController.receive.bind(proxyController)); app.route('/api-auth/api-key/:providerConfigKey').post(apiPublicAuth, apiAuthController.apiKey.bind(apiAuthController)); app.route('/api-auth/basic/:providerConfigKey').post(apiPublicAuth, apiAuthController.basic.bind(apiAuthController)); app.route('/app-store-auth/:providerConfigKey').post(apiPublicAuth, appStoreAuthController.auth.bind(appStoreAuthController)); diff --git a/packages/shared/lib/hooks/hooks.ts b/packages/shared/lib/hooks/hooks.ts index 4db9d5faf89..7955bb77cd0 100644 --- a/packages/shared/lib/hooks/hooks.ts +++ b/packages/shared/lib/hooks/hooks.ts @@ -15,25 +15,6 @@ import type { Result } from '../utils/result.js'; import { resultOk, resultErr } from '../utils/result.js'; import { NangoError } from '../utils/error.js'; -export const connectionCreationStart = async ({ - providerConfigKey, - connectionId, - environmentId, - activityLogId -}: { - providerConfigKey: string | undefined; - connectionId: string | undefined; - environmentId: number; - activityLogId: number | null; -}): Promise => { - if (!providerConfigKey || !connectionId || !activityLogId) { - return; - } - - console.log('connectionCreationStart', providerConfigKey, connectionId, environmentId, activityLogId); - return Promise.resolve(); -}; - export const connectionCreated = async ( connection: RecentlyCreatedConnection, provider: string, diff --git a/packages/shared/lib/services/sync/config/config.service.ts b/packages/shared/lib/services/sync/config/config.service.ts index 9387a9589ad..ccefb62462f 100644 --- a/packages/shared/lib/services/sync/config/config.service.ts +++ b/packages/shared/lib/services/sync/config/config.service.ts @@ -569,6 +569,55 @@ export async function getSyncConfigsWithConnectionsByEnvironmentId(environment_i return result; } +export async function getSyncConfigsWithConnectionsByEnvironmentIdAndProviderConfigKey( + providerConfigKey: string, + environment_id: number +): Promise { + const result = await schema() + .select( + `${TABLE}.id`, + `${TABLE}.sync_name`, + `${TABLE}.type`, + `${TABLE}.runs`, + `${TABLE}.models`, + `${TABLE}.version`, + `${TABLE}.updated_at`, + `${TABLE}.auto_start`, + `${TABLE}.pre_built`, + `${TABLE}.is_public`, + `${TABLE}.metadata`, + '_nango_configs.provider', + '_nango_configs.unique_key', + db.knex.raw( + `( + SELECT json_agg( + json_build_object( + 'connection_id', _nango_connections.connection_id, + 'metadata', _nango_connections.metadata + ) + ) + FROM _nango_connections + WHERE _nango_configs.environment_id = _nango_connections.environment_id + AND _nango_configs.unique_key = _nango_connections.provider_config_key + AND _nango_configs.deleted = false + AND _nango_connections.deleted = false + ) as connections + ` + ) + ) + .from(TABLE) + .join('_nango_configs', `${TABLE}.nango_config_id`, '_nango_configs.id') + .where({ + '_nango_configs.environment_id': environment_id, + '_nango_configs.unique_key': providerConfigKey, + active: true, + '_nango_configs.deleted': false, + [`${TABLE}.deleted`]: false + }); + + return result; +} + /** * Get Sync Configs By Provider Key * @desc grab all the sync configs by a provider key From 4fd9580ba89c32cbafc7bddaee6dbbe15e2c519e Mon Sep 17 00:00:00 2001 From: Khaliq Date: Wed, 3 Apr 2024 19:32:50 +0300 Subject: [PATCH 08/13] [nan-680] remove try catch --- packages/shared/lib/services/account.service.ts | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/packages/shared/lib/services/account.service.ts b/packages/shared/lib/services/account.service.ts index efcb444ff16..0805c6b2baf 100644 --- a/packages/shared/lib/services/account.service.ts +++ b/packages/shared/lib/services/account.service.ts @@ -34,17 +34,9 @@ class AccountService { } async getAccountByUUID(uuid: string): Promise { - try { - const result = await db.knex.select('*').from(`_nango_accounts`).where({ uuid }).first(); - return result || null; - } catch (e) { - errorManager.report(e, { - source: ErrorSourceEnum.PLATFORM, - operation: LogActionEnum.DATABASE - }); + const result = await db.knex.select('*').from(`_nango_accounts`).where({ uuid }).first(); - return null; - } + return result || null; } async getAccountAndEnvironmentIdByUUID(targetAccountUUID: string, targetEnvironment: string): Promise<{ accountId: number; environmentId: number } | null> { From 5d694fdb50e633942d8c586ca3ac2ba718bcde8c Mon Sep 17 00:00:00 2001 From: Khaliq Date: Wed, 3 Apr 2024 21:54:37 +0300 Subject: [PATCH 09/13] [nan-677] auth capping for create and import --- .../lib/controllers/connection.controller.ts | 16 ++++++++++ .../middleware/resource-capping.middleware.ts | 20 +++++++------ packages/shared/lib/hooks/hooks.ts | 30 +++++++++++++++++++ .../services/sync/config/config.service.ts | 5 ++-- packages/shared/lib/utils/error.ts | 7 +++++ 5 files changed, 66 insertions(+), 12 deletions(-) diff --git a/packages/server/lib/controllers/connection.controller.ts b/packages/server/lib/controllers/connection.controller.ts index 8e6d01a20ac..4d80ef2ee25 100644 --- a/packages/server/lib/controllers/connection.controller.ts +++ b/packages/server/lib/controllers/connection.controller.ts @@ -28,6 +28,7 @@ import { environmentService, accountService, connectionCreated as connectionCreatedHook, + connectionCreationStartCapCheck as connectionCreationStartCapCheckHook, slackNotificationService } from '@nangohq/shared'; import { getUserAccountAndEnvironmentFromSession } from '../utils/utils.js'; @@ -527,6 +528,21 @@ class ConnectionController { return; } + const account = await accountService.getAccountById(accountId); + + if (!account) { + errorManager.errRes(res, 'unknown_account'); + return; + } + + if (account.is_capped && provider_config_key) { + const isCapped = await connectionCreationStartCapCheckHook({ providerConfigKey: provider_config_key, environmentId }); + if (isCapped) { + errorManager.errRes(res, 'resource_capped'); + return; + } + } + const template = await configService.getTemplate(provider); let oAuthCredentials: ImportedCredentials; diff --git a/packages/server/lib/middleware/resource-capping.middleware.ts b/packages/server/lib/middleware/resource-capping.middleware.ts index eff5ef1d16a..f10cf94ed3a 100644 --- a/packages/server/lib/middleware/resource-capping.middleware.ts +++ b/packages/server/lib/middleware/resource-capping.middleware.ts @@ -1,8 +1,10 @@ import type { Request, Response, NextFunction } from 'express'; -import { getLogger } from '@nangohq/utils/dist/logger.js'; -import { getSyncConfigsWithConnectionsByEnvironmentIdAndProviderConfigKey, accountService, getEnvironmentAndAccountId, errorManager } from '@nangohq/shared'; - -const logger = getLogger('Resource Capping Middleware'); +import { + accountService, + getEnvironmentAndAccountId, + errorManager, + connectionCreationStartCapCheck as connectionCreationStartCapCheckHook +} from '@nangohq/shared'; export const authCheck = async (req: Request, res: Response, next: NextFunction) => { const { success, error, response } = await getEnvironmentAndAccountId(res, req); @@ -24,11 +26,11 @@ export const authCheck = async (req: Request, res: Response, next: NextFunction) const { providerConfigKey } = req.params; if (account.is_capped && providerConfigKey) { - logger.info('is capped'); - // account cannot have more than 3 connections for integrations with active scripts - console.log(providerConfigKey); - const syncConfigs = await getSyncConfigsWithConnectionsByEnvironmentIdAndProviderConfigKey(providerConfigKey, environmentId); - console.log(syncConfigs); + const isCapped = await connectionCreationStartCapCheckHook({ providerConfigKey, environmentId }); + if (isCapped) { + errorManager.errRes(res, 'resource_capped'); + return; + } } next(); diff --git a/packages/shared/lib/hooks/hooks.ts b/packages/shared/lib/hooks/hooks.ts index 7955bb77cd0..ae737d30a28 100644 --- a/packages/shared/lib/hooks/hooks.ts +++ b/packages/shared/lib/hooks/hooks.ts @@ -11,10 +11,40 @@ import integrationPostConnectionScript from '../integrations/scripts/connection/ import webhookService from '../services/notification/webhook.service.js'; import { SpanTypes } from '../utils/telemetry.js'; import { isCloud, isLocal, isEnterprise } from '../utils/temp/environment/detection.js'; +import { getSyncConfigsWithConnectionsByEnvironmentIdAndProviderConfigKey } from '../services/sync/config/config.service.js'; import type { Result } from '../utils/result.js'; import { resultOk, resultErr } from '../utils/result.js'; import { NangoError } from '../utils/error.js'; +export const connectionCreationStartCapCheck = async ({ + providerConfigKey, + environmentId +}: { + providerConfigKey: string | undefined; + environmentId: number; +}): Promise => { + if (!providerConfigKey) { + return false; + } + + const scriptConfigs = await getSyncConfigsWithConnectionsByEnvironmentIdAndProviderConfigKey(providerConfigKey, environmentId); + + let reachedCap = false; + + if (scriptConfigs.length > 0) { + for (const script of scriptConfigs) { + const { connections } = script; + + if (connections.length >= 3) { + reachedCap = true; + break; + } + } + } + + return reachedCap; +}; + export const connectionCreated = async ( connection: RecentlyCreatedConnection, provider: string, diff --git a/packages/shared/lib/services/sync/config/config.service.ts b/packages/shared/lib/services/sync/config/config.service.ts index ccefb62462f..eff2e597df5 100644 --- a/packages/shared/lib/services/sync/config/config.service.ts +++ b/packages/shared/lib/services/sync/config/config.service.ts @@ -572,7 +572,7 @@ export async function getSyncConfigsWithConnectionsByEnvironmentId(environment_i export async function getSyncConfigsWithConnectionsByEnvironmentIdAndProviderConfigKey( providerConfigKey: string, environment_id: number -): Promise { +): Promise<({ connections: { connection_id: string }[] } & SyncConfig & ProviderConfig)[]> { const result = await schema() .select( `${TABLE}.id`, @@ -592,8 +592,7 @@ export async function getSyncConfigsWithConnectionsByEnvironmentIdAndProviderCon `( SELECT json_agg( json_build_object( - 'connection_id', _nango_connections.connection_id, - 'metadata', _nango_connections.metadata + 'connection_id', _nango_connections.connection_id ) ) FROM _nango_connections diff --git a/packages/shared/lib/utils/error.ts b/packages/shared/lib/utils/error.ts index 2a25cb3886b..130f0e02b7a 100644 --- a/packages/shared/lib/utils/error.ts +++ b/packages/shared/lib/utils/error.ts @@ -559,6 +559,13 @@ export class NangoError extends Error { this.message = `Missing an account name for account login/signup.`; break; + case 'resource_capped': + this.status = 400; + // TODO docs link + this.message = + 'Free accounts cannot have > 3 connections for integrations with active scripts. Upgrade or deactivate the scripts to create more connections (https://docs.nango.dev/relevant-link).'; + break; + default: this.status = 500; this.type = 'unhandled_' + type; From 3c50b117372d43b5d45bb5287a3e7e4835604dd7 Mon Sep 17 00:00:00 2001 From: Khaliq Date: Thu, 4 Apr 2024 11:13:27 +0300 Subject: [PATCH 10/13] [nan-677] feedback --- .../lib/controllers/connection.controller.ts | 7 +++++++ packages/shared/lib/hooks/hooks.ts | 8 +++++--- .../lib/services/sync/config/config.service.ts | 16 +++------------- packages/shared/lib/utils/error.ts | 2 +- 4 files changed, 16 insertions(+), 17 deletions(-) diff --git a/packages/server/lib/controllers/connection.controller.ts b/packages/server/lib/controllers/connection.controller.ts index 4d80ef2ee25..20791c26b68 100644 --- a/packages/server/lib/controllers/connection.controller.ts +++ b/packages/server/lib/controllers/connection.controller.ts @@ -748,6 +748,13 @@ class ConnectionController { accountId ); + if (imported) { + updatedConnection = imported; + runHook = true; + } + } else if (template.auth_mode === ProviderAuthModes.None) { + const [imported] = await connectionService.upsertUnauthConnection(connection_id, provider_config_key, provider, environmentId, accountId); + if (imported) { updatedConnection = imported; runHook = true; diff --git a/packages/shared/lib/hooks/hooks.ts b/packages/shared/lib/hooks/hooks.ts index ae737d30a28..680b222ad3c 100644 --- a/packages/shared/lib/hooks/hooks.ts +++ b/packages/shared/lib/hooks/hooks.ts @@ -11,11 +11,13 @@ import integrationPostConnectionScript from '../integrations/scripts/connection/ import webhookService from '../services/notification/webhook.service.js'; import { SpanTypes } from '../utils/telemetry.js'; import { isCloud, isLocal, isEnterprise } from '../utils/temp/environment/detection.js'; -import { getSyncConfigsWithConnectionsByEnvironmentIdAndProviderConfigKey } from '../services/sync/config/config.service.js'; +import { getSyncConfigsWithConnections } from '../services/sync/config/config.service.js'; import type { Result } from '../utils/result.js'; import { resultOk, resultErr } from '../utils/result.js'; import { NangoError } from '../utils/error.js'; +const CONNECTIONS_WITH_SCRIPTS_CAP_LIMIT = 3; + export const connectionCreationStartCapCheck = async ({ providerConfigKey, environmentId @@ -27,7 +29,7 @@ export const connectionCreationStartCapCheck = async ({ return false; } - const scriptConfigs = await getSyncConfigsWithConnectionsByEnvironmentIdAndProviderConfigKey(providerConfigKey, environmentId); + const scriptConfigs = await getSyncConfigsWithConnections(providerConfigKey, environmentId); let reachedCap = false; @@ -35,7 +37,7 @@ export const connectionCreationStartCapCheck = async ({ for (const script of scriptConfigs) { const { connections } = script; - if (connections.length >= 3) { + if (connections.length >= CONNECTIONS_WITH_SCRIPTS_CAP_LIMIT) { reachedCap = true; break; } diff --git a/packages/shared/lib/services/sync/config/config.service.ts b/packages/shared/lib/services/sync/config/config.service.ts index eff2e597df5..d1952b20580 100644 --- a/packages/shared/lib/services/sync/config/config.service.ts +++ b/packages/shared/lib/services/sync/config/config.service.ts @@ -569,23 +569,13 @@ export async function getSyncConfigsWithConnectionsByEnvironmentId(environment_i return result; } -export async function getSyncConfigsWithConnectionsByEnvironmentIdAndProviderConfigKey( +export async function getSyncConfigsWithConnections( providerConfigKey: string, environment_id: number -): Promise<({ connections: { connection_id: string }[] } & SyncConfig & ProviderConfig)[]> { - const result = await schema() +): Promise<{ connections: { connection_id: string }[]; provider: string; unique_key: string }[]> { + const result = await db.knex .select( `${TABLE}.id`, - `${TABLE}.sync_name`, - `${TABLE}.type`, - `${TABLE}.runs`, - `${TABLE}.models`, - `${TABLE}.version`, - `${TABLE}.updated_at`, - `${TABLE}.auto_start`, - `${TABLE}.pre_built`, - `${TABLE}.is_public`, - `${TABLE}.metadata`, '_nango_configs.provider', '_nango_configs.unique_key', db.knex.raw( diff --git a/packages/shared/lib/utils/error.ts b/packages/shared/lib/utils/error.ts index 130f0e02b7a..a8a473befe3 100644 --- a/packages/shared/lib/utils/error.ts +++ b/packages/shared/lib/utils/error.ts @@ -563,7 +563,7 @@ export class NangoError extends Error { this.status = 400; // TODO docs link this.message = - 'Free accounts cannot have > 3 connections for integrations with active scripts. Upgrade or deactivate the scripts to create more connections (https://docs.nango.dev/relevant-link).'; + 'You have reached the maximum number of integrations with active scripts. Upgrade or deactivate the scripts to create more connections (https://docs.nango.dev/relevant-link).'; break; default: From 8656bc9ded5d6c79b5f80fe28fac75b879042db5 Mon Sep 17 00:00:00 2001 From: Khaliq Date: Thu, 4 Apr 2024 11:32:46 +0300 Subject: [PATCH 11/13] [nan-677] don't cap users actually until have a chance to set the correct values via the API --- packages/shared/lib/hooks/hooks.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/shared/lib/hooks/hooks.ts b/packages/shared/lib/hooks/hooks.ts index 680b222ad3c..18f1c9e7171 100644 --- a/packages/shared/lib/hooks/hooks.ts +++ b/packages/shared/lib/hooks/hooks.ts @@ -15,6 +15,9 @@ import { getSyncConfigsWithConnections } from '../services/sync/config/config.se import type { Result } from '../utils/result.js'; import { resultOk, resultErr } from '../utils/result.js'; import { NangoError } from '../utils/error.js'; +import { getLogger } from '../utils/temp/logger.js'; + +const logger = getLogger('hooks'); const CONNECTIONS_WITH_SCRIPTS_CAP_LIMIT = 3; @@ -31,14 +34,15 @@ export const connectionCreationStartCapCheck = async ({ const scriptConfigs = await getSyncConfigsWithConnections(providerConfigKey, environmentId); - let reachedCap = false; + const reachedCap = false; if (scriptConfigs.length > 0) { for (const script of scriptConfigs) { const { connections } = script; if (connections.length >= CONNECTIONS_WITH_SCRIPTS_CAP_LIMIT) { - reachedCap = true; + //reachedCap = true; + logger.info(`Reached cap for providerConfigKey: ${providerConfigKey} and environmentId: ${environmentId}`); break; } } From 469a893e5382a0c4c28531ec4b1ae98f3269a0df Mon Sep 17 00:00:00 2001 From: Khaliq Date: Thu, 4 Apr 2024 15:51:43 +0300 Subject: [PATCH 12/13] [nan-676] cap on activation --- .../server/lib/controllers/flow.controller.ts | 19 +++++++- packages/shared/lib/constants.ts | 2 + packages/shared/lib/hooks/hooks.ts | 10 ++--- .../shared/lib/services/connection.service.ts | 20 +++++++++ packages/shared/lib/utils/error.ts | 2 +- .../webapp/src/components/ui/ActionModal.tsx | 42 ++++++++++++++--- .../components/EnableDisableSync.tsx | 45 +++++++++++++++++-- 7 files changed, 123 insertions(+), 17 deletions(-) diff --git a/packages/server/lib/controllers/flow.controller.ts b/packages/server/lib/controllers/flow.controller.ts index a5a75433737..16294f4edc6 100644 --- a/packages/server/lib/controllers/flow.controller.ts +++ b/packages/server/lib/controllers/flow.controller.ts @@ -4,6 +4,7 @@ import type { IncomingPreBuiltFlowConfig, FlowDownloadBody, StandardNangoConfig import { flowService, accountService, + connectionService, getEnvironmentAndAccountId, errorManager, configService, @@ -95,7 +96,7 @@ class FlowController { return; } - const { environmentId } = response; + const { environmentId, accountId } = response; // config is an array for compatibility purposes, it will only ever have one item const [firstConfig] = config; @@ -111,6 +112,22 @@ class FlowController { return; } + const account = await accountService.getAccountById(accountId); + + if (!account) { + errorManager.errRes(res, 'unknown_account'); + return; + } + + if (account.is_capped && firstConfig?.providerConfigKey) { + const isCapped = await connectionService.shouldCapUsage({ providerConfigKey: firstConfig?.providerConfigKey, environmentId }); + + if (isCapped) { + errorManager.errRes(res, 'resource_capped'); + return; + } + } + const { success: preBuiltSuccess, error: preBuiltError, response: preBuiltResponse } = await deployPreBuiltSyncConfig(environmentId, config, ''); if (!preBuiltSuccess || preBuiltResponse === null) { diff --git a/packages/shared/lib/constants.ts b/packages/shared/lib/constants.ts index 206a1ef7a09..f980d2eb198 100644 --- a/packages/shared/lib/constants.ts +++ b/packages/shared/lib/constants.ts @@ -1,2 +1,4 @@ export const SYNC_TASK_QUEUE = 'nango-syncs'; export const WEBHOOK_TASK_QUEUE = 'nango-webhooks'; +//export const CONNECTIONS_WITH_SCRIPTS_CAP_LIMIT = 3; +export const CONNECTIONS_WITH_SCRIPTS_CAP_LIMIT = Infinity; diff --git a/packages/shared/lib/hooks/hooks.ts b/packages/shared/lib/hooks/hooks.ts index 18f1c9e7171..69e526be638 100644 --- a/packages/shared/lib/hooks/hooks.ts +++ b/packages/shared/lib/hooks/hooks.ts @@ -16,11 +16,10 @@ import type { Result } from '../utils/result.js'; import { resultOk, resultErr } from '../utils/result.js'; import { NangoError } from '../utils/error.js'; import { getLogger } from '../utils/temp/logger.js'; +import { CONNECTIONS_WITH_SCRIPTS_CAP_LIMIT } from '../constants.js'; const logger = getLogger('hooks'); -const CONNECTIONS_WITH_SCRIPTS_CAP_LIMIT = 3; - export const connectionCreationStartCapCheck = async ({ providerConfigKey, environmentId @@ -34,21 +33,18 @@ export const connectionCreationStartCapCheck = async ({ const scriptConfigs = await getSyncConfigsWithConnections(providerConfigKey, environmentId); - const reachedCap = false; - if (scriptConfigs.length > 0) { for (const script of scriptConfigs) { const { connections } = script; if (connections.length >= CONNECTIONS_WITH_SCRIPTS_CAP_LIMIT) { - //reachedCap = true; logger.info(`Reached cap for providerConfigKey: ${providerConfigKey} and environmentId: ${environmentId}`); - break; + return true; } } } - return reachedCap; + return false; }; export const connectionCreated = async ( diff --git a/packages/shared/lib/services/connection.service.ts b/packages/shared/lib/services/connection.service.ts index 67fae21e8ae..61bd8900d6f 100644 --- a/packages/shared/lib/services/connection.service.ts +++ b/packages/shared/lib/services/connection.service.ts @@ -48,6 +48,7 @@ import { Locking } from '../utils/lock/locking.js'; import { InMemoryKVStore } from '../utils/kvstore/InMemoryStore.js'; import { RedisKVStore } from '../utils/kvstore/RedisStore.js'; import type { KVStore } from '../utils/kvstore/KVStore.js'; +import { CONNECTIONS_WITH_SCRIPTS_CAP_LIMIT } from '../constants.js'; const logger = getLogger('Connection'); @@ -990,6 +991,25 @@ class ConnectionService { } } + public async shouldCapUsage({ providerConfigKey, environmentId }: { providerConfigKey: string | undefined; environmentId: number }): Promise { + if (!providerConfigKey) { + return false; + } + + const connections = await this.getConnectionsByEnvironmentAndConfig(environmentId, providerConfigKey); + + if (!connections) { + return false; + } + + if (connections.length >= CONNECTIONS_WITH_SCRIPTS_CAP_LIMIT) { + logger.info(`Reached cap for providerConfigKey: ${providerConfigKey} and environmentId: ${environmentId}`); + return true; + } + + return false; + } + private async getJWTCredentials( privateKey: string, url: string, diff --git a/packages/shared/lib/utils/error.ts b/packages/shared/lib/utils/error.ts index a8a473befe3..01ccfcce0aa 100644 --- a/packages/shared/lib/utils/error.ts +++ b/packages/shared/lib/utils/error.ts @@ -563,7 +563,7 @@ export class NangoError extends Error { this.status = 400; // TODO docs link this.message = - 'You have reached the maximum number of integrations with active scripts. Upgrade or deactivate the scripts to create more connections (https://docs.nango.dev/relevant-link).'; + 'You have reached the maximum number of integrations with active scripts. Upgrade or deactivate the scripts to create more connections (https://docs.nango.dev/REPLACE-ME).'; break; default: diff --git a/packages/webapp/src/components/ui/ActionModal.tsx b/packages/webapp/src/components/ui/ActionModal.tsx index a09f5e93737..c9f39c7afd3 100644 --- a/packages/webapp/src/components/ui/ActionModal.tsx +++ b/packages/webapp/src/components/ui/ActionModal.tsx @@ -11,9 +11,41 @@ interface ModalProps { modalTitle: string; modalAction: (() => void) | null; setVisible: (visible: boolean) => void; + modalOkTitle?: string; + modalCancelTitle?: string; + modalOkLink?: string | null; + modalCancelLink?: string | null; } -export default function ActionModal({ bindings, modalTitleColor, modalShowSpinner, modalContent, modalTitle, modalAction, setVisible }: ModalProps) { +export default function ActionModal({ + bindings, + modalTitleColor, + modalShowSpinner, + modalContent, + modalTitle, + modalAction, + setVisible, + modalOkTitle, + modalCancelTitle, + modalOkLink, + modalCancelLink +}: ModalProps) { + const modalOkAction = () => { + if (modalOkLink) { + window.open(modalOkLink, '_blank'); + } else { + modalAction && modalAction(); + } + }; + + const modalCancelAction = () => { + if (modalCancelLink) { + window.open(modalCancelLink, '_blank'); + } else { + setVisible(false); + } + }; + return (
@@ -34,12 +66,12 @@ export default function ActionModal({ bindings, modalTitleColor, modalShowSpinne
{modalAction && ( - )} -
diff --git a/packages/webapp/src/pages/Integration/components/EnableDisableSync.tsx b/packages/webapp/src/pages/Integration/components/EnableDisableSync.tsx index 735ea397fd3..95547fa5b96 100644 --- a/packages/webapp/src/pages/Integration/components/EnableDisableSync.tsx +++ b/packages/webapp/src/pages/Integration/components/EnableDisableSync.tsx @@ -39,11 +39,28 @@ export default function EnableDisableSync({ const [modalTitle, setModalTitle] = useState(''); const [modalContent, setModalContent] = useState(''); + const [modalOkButtonTitle, setModalOkButtonTitle] = useState('Confirm'); + const [modalCancelButtonTitle, setModalCancelButtonTitle] = useState('Cancel'); + const [modalOkButtonLink, setModalOkButtonLink] = useState(null); + const [modalCancelButtonLink, setModalCancelButtonLink] = useState(null); const [modalAction, setModalAction] = useState<(() => void) | null>(null); const [modalShowSpinner, setModalShowSpinner] = useState(false); const [modalTitleColor, setModalTitleColor] = useState('text-white'); + const resetModal = () => { + setModalTitle(''); + setModalContent(''); + setModalOkButtonTitle('Confirm'); + setModalCancelButtonTitle('Cancel'); + setModalOkButtonLink(null); + setModalCancelButtonLink(null); + setModalAction(null); + setModalShowSpinner(false); + setModalTitleColor('text-white'); + }; + const enableSync = (flow: Flow) => { + resetModal(); setModalTitle(`Enable ${flow.type}?`); setModalTitleColor('text-white'); const content = @@ -96,9 +113,26 @@ export default function EnableDisableSync({ reload(); } else { const payload = await res?.json(); - toast.error(payload.error, { - position: toast.POSITION.BOTTOM_CENTER - }); + if (payload.type === 'resource_capped') { + setModalShowSpinner(false); + setModalTitleColor('text-white'); + setModalTitle('You’ve reached your connections limit!'); + setModalContent( + `Scripts are a paid feature. You can only use them with 3 connections or less. + Upgrade or delete some connections to activate this script.` + ); + setModalOkButtonTitle('Upgrade'); + setModalCancelButtonTitle('Learn more'); + setModalOkButtonLink('https://nango.dev/chat'); + setModalCancelButtonLink('https://docs.nango.dev/REPLACE-ME'); + setVisible(true); + + return; + } else { + toast.error(payload.error, { + position: toast.POSITION.BOTTOM_CENTER + }); + } } setModalShowSpinner(false); if (setIsEnabling) { @@ -108,6 +142,7 @@ export default function EnableDisableSync({ }; const disableSync = (flow: Flow) => { + resetModal(); if (!flow.is_public) { const title = 'Custom syncs cannot be disabled from the UI'; const message = flow.pre_built @@ -182,6 +217,10 @@ export default function EnableDisableSync({ modalShowSpinner={modalShowSpinner} modalTitleColor={modalTitleColor} setVisible={setVisible} + modalOkTitle={modalOkButtonTitle} + modalCancelTitle={modalCancelButtonTitle} + modalOkLink={modalOkButtonLink} + modalCancelLink={modalCancelButtonLink} /> {showSpinner && (!('version' in flow) || flow.version === null) && modalShowSpinner && ( From b597d66fb57c68f37d649885b8882ea585426980 Mon Sep 17 00:00:00 2001 From: Khaliq Date: Fri, 5 Apr 2024 19:40:13 +0300 Subject: [PATCH 13/13] [nan-676] make providerConfigKey required --- packages/shared/lib/services/connection.service.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/shared/lib/services/connection.service.ts b/packages/shared/lib/services/connection.service.ts index 3473e89f4b0..0af1d7f3601 100644 --- a/packages/shared/lib/services/connection.service.ts +++ b/packages/shared/lib/services/connection.service.ts @@ -991,11 +991,7 @@ class ConnectionService { } } - public async shouldCapUsage({ providerConfigKey, environmentId }: { providerConfigKey: string | undefined; environmentId: number }): Promise { - if (!providerConfigKey) { - return false; - } - + public async shouldCapUsage({ providerConfigKey, environmentId }: { providerConfigKey: string; environmentId: number }): Promise { const connections = await this.getConnectionsByEnvironmentAndConfig(environmentId, providerConfigKey); if (!connections) {