diff --git a/examples/test.js b/examples/test.js index 46d37efa..f18b9b8a 100644 --- a/examples/test.js +++ b/examples/test.js @@ -80,13 +80,14 @@ let fdkExtension = setupFdk({ } }); -app.use(fdkExtension.fdkHandler); + app.use('/_healthz', (req, res, next) => { res.json({ "ok": "ok" }); }); +app.use('/', fdkExtension.fdkHandler); fdkExtension.apiRoutes.get("/test/routes", async (req, res, next) => { try { let data = await req.platformClient.lead.getTickets(); diff --git a/express/api_routes.js b/express/api_routes.js index c08dc29f..91545f06 100644 --- a/express/api_routes.js +++ b/express/api_routes.js @@ -1,16 +1,20 @@ 'use strict'; -const { extension } = require('./extension'); const express = require('express'); const { sessionMiddleware } = require('./middleware/session_middleware'); const { ApplicationConfig, ApplicationClient } = require("@gofynd/fdk-client-javascript"); +const { ExtensionFactory } = require('./extension_factory'); -function setupProxyRoutes() { +function setupProxyRoutes(extension) { const apiRoutes = express.Router({ mergeParams: true }); const applicationProxyRoutes = express.Router({ mergeParams: true }); - applicationProxyRoutes.use(async (req, res, next) => { + applicationProxyRoutes.use( "/" ,async (req, res, next) => { try { + const clusterId = req.query.cluster_domain || req.query.cluster_url?.replace("https://", "").replace("http://", ""); + if (clusterId) { + extension = ExtensionFactory.getExtension(clusterId) + } if(req.headers["x-user-data"]) { req.user = JSON.parse(req.headers["x-user-data"]); req.user.user_id = req.user._id; @@ -30,8 +34,12 @@ function setupProxyRoutes() { } }); - apiRoutes.use(sessionMiddleware(true), async (req, res, next) => { + apiRoutes.use( "/" , sessionMiddleware(extension, true), async (req, res, next) => { try { + const clusterId = req.query.cluster_domain || req.query.cluster_url?.replace("https://", "").replace("http://", ""); + if (clusterId) { + extension = ExtensionFactory.getExtension(clusterId) + } const client = await extension.getPlatformClient(req.fdkSession.company_id, req.fdkSession); req.platformClient = client; req.extension = extension; diff --git a/express/error_code.js b/express/error_code.js index 99334894..52e65cf8 100644 --- a/express/error_code.js +++ b/express/error_code.js @@ -54,6 +54,12 @@ class FdkWebhookHandlerNotFound extends Error { } } +class FdkInvalidCluster extends Error { + constructor(message) { + super(message); + } +} + module.exports = { FdkInvalidExtensionConfig, @@ -64,5 +70,6 @@ module.exports = { FdkInvalidWebhookConfig, FdkWebhookRegistrationError, FdkWebhookProcessError, - FdkWebhookHandlerNotFound + FdkWebhookHandlerNotFound, + FdkInvalidCluster }; \ No newline at end of file diff --git a/express/extension.js b/express/extension.js index 59c7e11e..a5900344 100644 --- a/express/extension.js +++ b/express/extension.js @@ -7,6 +7,7 @@ const { WebhookRegistry } = require('./webhook'); const logger = require('./logger'); const { fdkAxios } = require('@gofynd/fdk-client-javascript/sdk/common/AxiosHelper'); const { version } = require('./../package.json'); +const SessionStorage = require("./session/session_storage"); const { RetryManger } = require("./retry_manager") class Extension { @@ -19,7 +20,9 @@ class Extension { this.access_mode = null; this.cluster = "https://api.fynd.com"; this.webhookRegistry = null; + this.sessionStore = null; this._isInitialized = false; + this._clusterId = null; this._retryManager = new RetryManger(); } @@ -34,6 +37,7 @@ class Extension { this.storage = data.storage; + this.sessionStorage = new SessionStorage(this.storage); if (!data.api_key) { throw new FdkInvalidExtensionConfig("Invalid api_key"); } @@ -56,6 +60,7 @@ class Extension { throw new FdkInvalidExtensionConfig("Invalid cluster value. Invalid value: " + data.cluster); } this.cluster = data.cluster; + this._clusterId = this.cluster.replace("https://", ""); } this.webhookRegistry = new WebhookRegistry(this._retryManager); @@ -87,6 +92,10 @@ class Extension { return this._isInitialized; } + get clusterId(){ + return this._clusterId; + } + verifyScopes(scopes, extensionData) { const missingScopes = scopes.filter(val => extensionData.scope.indexOf(val) === -1); if (!scopes || scopes.length <= 0 || missingScopes.length) { @@ -122,7 +131,6 @@ class Extension { if (!this._isInitialized){ await this.initialize(this.configData); } - const SessionStorage = require('./session/session_storage'); let platformConfig = await this.getPlatformConfig(companyId); platformConfig.oauthClient.setToken(session); @@ -135,7 +143,7 @@ class Extension { const renewTokenRes = await platformConfig.oauthClient.renewAccessToken(session.access_mode === 'offline'); renewTokenRes.access_token_validity = platformConfig.oauthClient.token_expires_at; session.updateToken(renewTokenRes); - await SessionStorage.saveSession(session); + await this.sessionStorage.saveSession(session); logger.debug(`Access token renewed for company ${companyId} with response ${logger.safeStringify(renewTokenRes)}`); } } @@ -188,9 +196,6 @@ class Extension { } } - -const extension = new Extension(); - module.exports = { - extension + Extension }; diff --git a/express/extension_factory.js b/express/extension_factory.js new file mode 100644 index 00000000..38355c27 --- /dev/null +++ b/express/extension_factory.js @@ -0,0 +1,40 @@ +const { FdkInvalidCluster } = require("./error_code"); +const { Extension } = require("./extension") + +class ExtensionFactory { + static _extensionMap = {}; + static _defaultExt = null; + + static getExtension(clusterId) { + const clusterExt = ExtensionFactory._extensionMap[clusterId] + if (clusterId !== null && !clusterExt) { + throw new FdkInvalidCluster(`Extension instance not found for clusterId ${clusterId}`); + } + return clusterExt; + } + + static defaultExtInstance() { + return ExtensionFactory._defaultExt; + } + + static async initializeExtension(clusterData, clusterId = null) { + const promises = []; + for (let extConfig of clusterData) { + const extInstance = new Extension(); + if(clusterId != null && clusterId !== extConfig.cluster_id) { + continue; + } + if (!ExtensionFactory._defaultExt) { + ExtensionFactory._defaultExt = extInstance; + } + const cluster_id = clusterId || extConfig.cluster.replace("https://", "").replace("http://", ""); + ExtensionFactory._extensionMap[cluster_id] = extInstance; + promises.push(extInstance.initialize(extConfig)) + } + return Promise.all(promises); + } +} + +module.exports = { + ExtensionFactory +}; \ No newline at end of file diff --git a/express/index.js b/express/index.js index 3b0a180c..28b99bb7 100644 --- a/express/index.js +++ b/express/index.js @@ -1,48 +1,68 @@ 'use strict'; -const { extension } = require('./extension'); +const {ExtensionFactory} = require('./extension_factory'); const setupRoutes = require("./routes"); const { setupProxyRoutes } = require("./api_routes"); const Session = require("./session/session"); -const SessionStorage = require("./session/session_storage"); const { ApplicationConfig, ApplicationClient } = require("@gofynd/fdk-client-javascript"); const logger = require('./logger'); function setupFdk(data, syncInitialization) { + const multiClusterMode = data.cluster_config !== undefined; + const clusterId = data.cluster?.replace("https://", "").replace("http://", ""); if (data.debug) { logger.transports[0].level = 'debug'; } - const promiseInit = extension.initialize(data) - .catch(err=>{ - logger.error(err); - throw err; - }); + + const promiseInit = ExtensionFactory.initializeExtension(multiClusterMode? data.cluster_config: [data]).catch(err=>{ + logger.error(err); + throw err; + }); + + const extension = !multiClusterMode? ExtensionFactory.defaultExtInstance(): ExtensionFactory.getExtension(clusterId); + let router = setupRoutes(extension); - let { apiRoutes, applicationProxyRoutes } = setupProxyRoutes(); + let { apiRoutes, applicationProxyRoutes } = setupProxyRoutes(extension); - async function getPlatformClient(companyId) { + async function getPlatformClient(companyId, clusterId = null) { + let clusterExt = extension; + if (clusterId) { + clusterExt = ExtensionFactory.getExtension(clusterId) + } let client = null; - if (!extension.isOnlineAccessMode()) { + if (!clusterExt.isOnlineAccessMode()) { let sid = Session.generateSessionId(false, { - cluster: extension.cluster, + cluster: clusterExt.cluster, companyId: companyId }); - let session = await SessionStorage.getSession(sid); - client = await extension.getPlatformClient(companyId, session); + let session = await clusterExt.sessionStorage.getSession(sid); + client = await clusterExt.getPlatformClient(companyId, session); } return client; } - async function getApplicationClient(applicationId, applicationToken) { + async function getApplicationClient(applicationId, applicationToken, clusterId = null) { + let clusterExt = extension; + if (clusterId) { + clusterExt = ExtensionFactory.getExtension(clusterId) + } let applicationConfig = new ApplicationConfig({ applicationID: applicationId, applicationToken: applicationToken, - domain: extension.cluster + domain: clusterExt.cluster }); let applicationClient = new ApplicationClient(applicationConfig); return applicationClient; } + function getWebhookRegistry(clusterId) { + let clusterExt = extension; + if (clusterId) { + clusterExt = ExtensionFactory.getExtension(clusterId) + } + return clusterExt.webhookRegistry; + } + const configInstance = { fdkHandler: router, extension: extension, @@ -50,7 +70,8 @@ function setupFdk(data, syncInitialization) { webhookRegistry: extension.webhookRegistry, applicationProxyRoutes: applicationProxyRoutes, getPlatformClient: getPlatformClient, - getApplicationClient: getApplicationClient + getApplicationClient: getApplicationClient, + getWebhookRegistry: getWebhookRegistry }; return syncInitialization? promiseInit.then(()=>configInstance).catch(()=>configInstance): configInstance; diff --git a/express/middleware/session_middleware.js b/express/middleware/session_middleware.js index 687e9b51..bd6e0198 100644 --- a/express/middleware/session_middleware.js +++ b/express/middleware/session_middleware.js @@ -1,14 +1,18 @@ 'use strict'; +const { ExtensionFactory } = require('../extension_factory'); const { SESSION_COOKIE_NAME } = require('./../constants'); -const SessionStorage = require("../session/session_storage"); -function sessionMiddleware(strict) { +function sessionMiddleware(extension, strict) { return async (req, res, next) => { try { + const clusterId = req.query.cluster_domain; + if (clusterId) { + extension = ExtensionFactory.getExtension(clusterId) + } const companyId = req.headers['x-company-id'] || req.query['company_id']; const compCookieName = `${SESSION_COOKIE_NAME}_${companyId}` let sessionId = req.signedCookies[compCookieName]; - req.fdkSession = await SessionStorage.getSession(sessionId); + req.fdkSession = await extension.sessionStorage.getSession(sessionId); if(strict && !req.fdkSession) { return res.status(401).json({ "message": "unauthorized" }); diff --git a/express/routes.js b/express/routes.js index 2abfa1b7..d5757c5a 100644 --- a/express/routes.js +++ b/express/routes.js @@ -2,25 +2,40 @@ const express = require('express'); const { v4: uuidv4 } = require("uuid"); const Session = require("./session/session"); -const SessionStorage = require("./session/session_storage"); const { FdkSessionNotFoundError, FdkInvalidOAuthError } = require("./error_code"); const { SESSION_COOKIE_NAME } = require('./constants'); const { sessionMiddleware } = require('./middleware/session_middleware'); +const { ExtensionFactory } = require('./extension_factory'); const logger = require('./logger'); const FdkRoutes = express.Router(); function setupRoutes(ext) { - let storage = ext.storage; - let callbacks = ext.callbacks; - FdkRoutes.get("/fp/install", async (req, res, next) => { // ?company_id=1&client_id=123313112122 try { + const cluster_id = req.query.cluster_domain || req.query.cluster_url?.replace("https://", "").replace("http://", ""); + if (cluster_id) { + ext = ExtensionFactory.getExtension(cluster_id) + } let companyId = parseInt(req.query.company_id); let platformConfig = await ext.getPlatformConfig(companyId); let session; + if (ext.isOnlineAccessMode()) { + session = new Session(Session.generateSessionId(true)); + } else { + let sid = Session.generateSessionId(false, { + cluster: ext.cluster, + companyId: companyId + }); + session = await ext.sessionStorage.getSession(sid); + if (!session) { + session = new Session(sid); + } else if (session.extension_id !== ext.api_key) { + session = new Session(sid); + } + } session = new Session(Session.generateSessionId(true)); @@ -68,7 +83,7 @@ function setupRoutes(ext) { state: session.state, access_mode: 'online' // Always generate online mode token for extension launch }); - await SessionStorage.saveSession(session); + await ext.sessionStorage.saveSession(session); logger.debug(`Redirecting after install callback to url: ${redirectUrl}`); res.redirect(redirectUrl); } catch (error) { @@ -76,9 +91,13 @@ function setupRoutes(ext) { } }); - FdkRoutes.get("/fp/auth", sessionMiddleware(false), async (req, res, next) => { + FdkRoutes.get("/fp/auth", sessionMiddleware(ext, false), async (req, res, next) => { // ?code=ddjfhdsjfsfh&client_id=jsfnsajfhkasf&company_id=1&state=jashoh try { + const cluster_id = req.query.cluster_domain || req.query.cluster_url?.replace("https://", "").replace("http://", ""); + if (cluster_id) { + ext = ExtensionFactory.getExtension(cluster_id) + } if (!req.fdkSession) { throw new FdkSessionNotFoundError("Can not complete oauth process as session not found"); } @@ -98,7 +117,7 @@ function setupRoutes(ext) { token.access_token_validity = sessionExpires.getTime(); req.fdkSession.updateToken(token); - await SessionStorage.saveSession(req.fdkSession); + await ext.sessionStorage.saveSession(req.fdkSession); // Generate separate access token for offline mode if (!ext.isOnlineAccessMode()) { @@ -107,7 +126,7 @@ function setupRoutes(ext) { cluster: ext.cluster, companyId: companyId }); - let session = await SessionStorage.getSession(sid); + let session = await ext.sessionStorage.getSession(sid); if (!session) { session = new Session(sid); } else if (session.extension_id !== ext.api_key) { @@ -124,7 +143,7 @@ function setupRoutes(ext) { offlineTokenRes.access_mode = 'offline'; session.updateToken(offlineTokenRes); - await SessionStorage.saveSession(session); + await ext.sessionStorage.saveSession(session); } @@ -154,11 +173,14 @@ function setupRoutes(ext) { }); - FdkRoutes.post("/fp/auto_install", sessionMiddleware(false), async (req, res, next) => { + FdkRoutes.post("/fp/auto_install", sessionMiddleware(ext, false), async (req, res, next) => { try { let { company_id, code } = req.body; - + const cluster_id = req.query.cluster_domain || req.query.cluster_url?.replace("https://", "").replace("http://", ""); + if (cluster_id) { + ext = ExtensionFactory.getExtension(cluster_id) + } logger.debug(`Extension auto install started for company: ${company_id} on company creation.`); let platformConfig = await ext.getPlatformConfig(company_id); @@ -166,8 +188,8 @@ function setupRoutes(ext) { cluster: ext.cluster, companyId: company_id }); - - let session = await SessionStorage.getSession(sid); + + let session = await ext.sessionStorage.getSession(sid); if (!session) { session = new Session(sid); } else if (session.extension_id !== ext.api_key) { @@ -185,9 +207,9 @@ function setupRoutes(ext) { session.updateToken(offlineTokenRes); if (!ext.isOnlineAccessMode()) { - await SessionStorage.saveSession(session); + await ext.sessionStorage.saveSession(session); } - + if (ext.webhookRegistry.isInitialized && ext.webhookRegistry.isSubscribeOnInstall) { const client = await ext.getPlatformClient(company_id, session); await ext.webhookRegistry.syncEvents(client, null, true).catch((err) => { @@ -197,7 +219,7 @@ function setupRoutes(ext) { logger.debug(`Extension installed for company: ${company_id} on company creation.`); if (ext.callbacks.auto_install) { await ext.callbacks.auto_install(req); - } + } res.json({ message: "success" }); } catch (error) { logger.error(error); @@ -208,13 +230,20 @@ function setupRoutes(ext) { FdkRoutes.post("/fp/uninstall", async (req, res, next) => { try { let { company_id } = req.body; + const cluster_id = req.query.cluster_domain || req.query.cluster_url?.replace("https://", "").replace("http://", ""); + if (cluster_id) { + ext = ExtensionFactory.getExtension(cluster_id) + } let sid; if (!ext.isOnlineAccessMode()) { sid = Session.generateSessionId(false, { cluster: ext.cluster, companyId: company_id }); - await SessionStorage.deleteSession(sid); + let session = await ext.sessionStorage.getSession(sid); + const client = await ext.getPlatformClient(company_id, session); + req.platformClient = client; + await ext.sessionStorage.deleteSession(sid); } req.extension = ext; await ext.callbacks.uninstall(req); diff --git a/express/session/session_storage.js b/express/session/session_storage.js index 3ac0ea0f..f876243e 100644 --- a/express/session/session_storage.js +++ b/express/session/session_storage.js @@ -1,24 +1,24 @@ 'use strict'; const Session = require("./session"); -const { extension } = require("./../extension"); const logger = require("../logger"); class SessionStorage { - constructor() { + constructor(storage) { + this.storage = storage; } - static async saveSession(session) { + async saveSession(session) { if(session.expires) { let ttl = (new Date() - session.expires) / 1000; ttl = Math.abs(Math.round(Math.min(ttl, 0))); - return extension.storage.setex(session.id, JSON.stringify(session.toJSON()), ttl); + return this.storage.setex(session.id, JSON.stringify(session.toJSON()), ttl); } else { - return extension.storage.set(session.id, JSON.stringify(session.toJSON())); + return this.storage.set(session.id, JSON.stringify(session.toJSON())); } } - static async getSession(sessionId) { - let session = await extension.storage.get(sessionId); + async getSession(sessionId) { + let session = await this.storage.get(sessionId); if(session) { session = JSON.parse(session); session = Session.cloneSession(sessionId, session, false); @@ -29,8 +29,8 @@ class SessionStorage { return session; } - static async deleteSession(sessionId) { - return extension.storage.del(sessionId); + async deleteSession(sessionId) { + return this.storage.del(sessionId); } } diff --git a/express/webhook.js b/express/webhook.js index a9648253..34b723e2 100644 --- a/express/webhook.js +++ b/express/webhook.js @@ -291,6 +291,7 @@ class WebhookRegistry { } try { const { body } = req; + const clusterId = body.referer || req.query.cluster_domain; if (body.event.name === TEST_WEBHOOK_EVENT_NAME) { return; } @@ -306,7 +307,7 @@ class WebhookRegistry { if (typeof extHandler === 'function') { logger.debug(`Webhook event received for company: ${req.body.company_id}, application: ${req.body.application_id || ''}, event name: ${eventName}`); - await extHandler(eventName, req.body, req.body.company_id, req.body.application_id); + await extHandler(eventName, req.body, req.body.company_id, req.body.application_id, clusterId); } else { throw new FdkWebhookHandlerNotFound(`Webhook handler not assigned: ${categoryEventName}`);