From 83dd35299c48c26ade1e0d7e255b6831db84aa66 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Tue, 23 May 2023 22:28:41 +0300 Subject: [PATCH] Added add/remove/get organization payment methods and get cloud plans from license server --- .../controllers/v1/cloudProductsController.ts | 34 ++++++++ backend/src/ee/controllers/v1/index.ts | 4 +- .../controllers/v1/organizationsController.ts | 81 +++++++++++++++++-- backend/src/ee/helpers/organizations.ts | 39 --------- backend/src/ee/routes/v1/cloudProducts.ts | 20 +++++ backend/src/ee/routes/v1/index.ts | 4 +- backend/src/ee/routes/v1/organizations.ts | 46 ++++++++++- backend/src/index.ts | 4 +- 8 files changed, 182 insertions(+), 50 deletions(-) create mode 100644 backend/src/ee/controllers/v1/cloudProductsController.ts delete mode 100644 backend/src/ee/helpers/organizations.ts create mode 100644 backend/src/ee/routes/v1/cloudProducts.ts diff --git a/backend/src/ee/controllers/v1/cloudProductsController.ts b/backend/src/ee/controllers/v1/cloudProductsController.ts new file mode 100644 index 000000000..54584ce82 --- /dev/null +++ b/backend/src/ee/controllers/v1/cloudProductsController.ts @@ -0,0 +1,34 @@ +import * as Sentry from '@sentry/node'; +import { Request, Response } from 'express'; +import { EELicenseService } from '../../services'; +import { getLicenseServerUrl } from '../../../config'; +import { licenseServerKeyRequest } from '../../../config/request'; + +/** + * Return available cloud product information. + * Note: Nicely formatted to easily construct a table from + * @param req + * @param res + * @returns + */ +export const getCloudProducts = async (req: Request, res: Response) => { + try { + const billingCycle = req.query['billing-cycle'] as string; + + if (EELicenseService.instanceType === 'cloud') { + const { data } = await licenseServerKeyRequest.get( + `${await getLicenseServerUrl()}/api/license-server/v1/cloud-products?billing-cycle=${billingCycle}` + ); + + return res.status(200).send(data); + } + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + } + + return res.status(200).send({ + head: [], + rows: [] + }); +} \ No newline at end of file diff --git a/backend/src/ee/controllers/v1/index.ts b/backend/src/ee/controllers/v1/index.ts index b9618495f..bf3992b17 100644 --- a/backend/src/ee/controllers/v1/index.ts +++ b/backend/src/ee/controllers/v1/index.ts @@ -5,6 +5,7 @@ import * as organizationsController from './organizationsController'; import * as workspaceController from './workspaceController'; import * as actionController from './actionController'; import * as membershipController from './membershipController'; +import * as cloudProductsController from './cloudProductsController'; export { stripeController, @@ -13,5 +14,6 @@ export { organizationsController, workspaceController, actionController, - membershipController + membershipController, + cloudProductsController } \ No newline at end of file diff --git a/backend/src/ee/controllers/v1/organizationsController.ts b/backend/src/ee/controllers/v1/organizationsController.ts index 1f9cf1080..1a19eef2d 100644 --- a/backend/src/ee/controllers/v1/organizationsController.ts +++ b/backend/src/ee/controllers/v1/organizationsController.ts @@ -1,15 +1,82 @@ -import { Types } from 'mongoose'; +import * as Sentry from '@sentry/node'; import { Request, Response } from 'express'; -import { getOrganizationPlanHelper } from '../../helpers/organizations'; +import { getLicenseServerUrl } from '../../../config'; +import { licenseServerKeyRequest } from '../../../config/request'; +import { EELicenseService } from '../../services'; +/** + * Return the organization's current plan and allowed feature set + */ export const getOrganizationPlan = async (req: Request, res: Response) => { - const { organizationId } = req.params; + try { + if (EELicenseService.instanceType === 'cloud') { + // instance of Infisical is a cloud instance - const plan = await getOrganizationPlanHelper({ - organizationId: new Types.ObjectId(organizationId) + const cachedPlan = EELicenseService.localFeatureSet.get(req.organization._id.toString()); + if (cachedPlan) return cachedPlan; + + const { data: { currentPlan } } = await licenseServerKeyRequest.get( + `${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/cloud-plan` + ); + + // cache fetched plan for organization + EELicenseService.localFeatureSet.set(req.organization._id.toString(), currentPlan); + + return res.status(200).send({ + plan: currentPlan + }); + } + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + } + + return res.status(200).send({ + plan: EELicenseService.globalFeatureSet }); +} + +/** + * Return the organization's payment methods on file + */ +export const getOrganizationPmtMethods = async (req: Request, res: Response) => { + const { data: { pmtMethods } } = await licenseServerKeyRequest.get( + `${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details/payment-methods` + ); + + return res.status(200).send({ + pmtMethods + }); +} + +/** + * Return a Stripe session URL to add payment method for organization + */ +export const addOrganizationPmtMethod = async (req: Request, res: Response) => { + const { + success_url, + cancel_url + } = req.body; + + const { data: { url } } = await licenseServerKeyRequest.post( + `${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details/payment-methods`, + { + success_url, + cancel_url + } + ); return res.status(200).send({ - plan - }); + url + }); +} + +export const deleteOrganizationPmtMethod = async (req: Request, res: Response) => { + const { pmtMethodId } = req.params; + + const { data } = await licenseServerKeyRequest.delete( + `${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details/payment-methods/${pmtMethodId}`, + ); + + return res.status(200).send(data); } \ No newline at end of file diff --git a/backend/src/ee/helpers/organizations.ts b/backend/src/ee/helpers/organizations.ts deleted file mode 100644 index 80b1048be..000000000 --- a/backend/src/ee/helpers/organizations.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Types } from 'mongoose'; -import * as Sentry from '@sentry/node'; -import { Organization } from '../../models'; -import { EELicenseService } from '../services'; -import { getLicenseServerUrl } from '../../config'; -import { licenseServerKeyRequest } from '../../config/request'; -import { OrganizationNotFoundError } from '../../utils/errors'; - -export const getOrganizationPlanHelper = async ({ - organizationId -}: { - organizationId: Types.ObjectId; -}) => { - try { - if (EELicenseService.instanceType === 'cloud') { - // instance of Infisical is a cloud instance - - const organization = await Organization.findById(organizationId); - if (!organization) throw OrganizationNotFoundError(); - - const cachedPlan = EELicenseService.localFeatureSet.get(organizationId.toString()); - if (cachedPlan) return cachedPlan; - - const { data: { currentPlan } } = await licenseServerKeyRequest.get( - `${await getLicenseServerUrl()}/api/license-server/v1/customers/${organization.customerId}/cloud-plan` - ); - - // cache fetched plan for organization - EELicenseService.localFeatureSet.set(organizationId.toString(), currentPlan); - return currentPlan; - } - - return EELicenseService.globalFeatureSet; - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - return EELicenseService.globalFeatureSet; - } -} \ No newline at end of file diff --git a/backend/src/ee/routes/v1/cloudProducts.ts b/backend/src/ee/routes/v1/cloudProducts.ts new file mode 100644 index 000000000..73af00aca --- /dev/null +++ b/backend/src/ee/routes/v1/cloudProducts.ts @@ -0,0 +1,20 @@ +import express from 'express'; +const router = express.Router(); +import { + requireAuth, + validateRequest +} from '../../../middleware'; +import { query } from 'express-validator'; +import { cloudProductsController } from '../../controllers/v1'; + +router.get( + '/', + requireAuth({ + acceptedAuthModes: ['jwt', 'apiKey'] + }), + query('billing-cycle').exists().isIn(['monthly', 'yearly']), + validateRequest, + cloudProductsController.getCloudProducts +); + +export default router; \ No newline at end of file diff --git a/backend/src/ee/routes/v1/index.ts b/backend/src/ee/routes/v1/index.ts index 9b4d30b1b..7568e45d6 100644 --- a/backend/src/ee/routes/v1/index.ts +++ b/backend/src/ee/routes/v1/index.ts @@ -3,11 +3,13 @@ import secretSnapshot from './secretSnapshot'; import organizations from './organizations'; import workspace from './workspace'; import action from './action'; +import cloudProducts from './cloudProducts'; export { secret, secretSnapshot, organizations, workspace, - action + action, + cloudProducts } \ No newline at end of file diff --git a/backend/src/ee/routes/v1/organizations.ts b/backend/src/ee/routes/v1/organizations.ts index 3f208b3c2..b41e49334 100644 --- a/backend/src/ee/routes/v1/organizations.ts +++ b/backend/src/ee/routes/v1/organizations.ts @@ -5,7 +5,7 @@ import { requireOrganizationAuth, validateRequest } from '../../../middleware'; -import { param } from 'express-validator'; +import { param, body } from 'express-validator'; import { organizationsController } from '../../controllers/v1'; import { OWNER, ADMIN, MEMBER, ACCEPTED @@ -25,4 +25,48 @@ router.get( organizationsController.getOrganizationPlan ); +router.get( + '/:organizationId/billing-details/payment-methods', + requireAuth({ + acceptedAuthModes: ['jwt', 'apiKey'] + }), + requireOrganizationAuth({ + acceptedRoles: [OWNER, ADMIN, MEMBER], + acceptedStatuses: [ACCEPTED] + }), + param('organizationId').exists().trim(), + validateRequest, + organizationsController.getOrganizationPmtMethods +); + +router.post( + '/:organizationId/billing-details/payment-methods', + requireAuth({ + acceptedAuthModes: ['jwt', 'apiKey'] + }), + requireOrganizationAuth({ + acceptedRoles: [OWNER, ADMIN, MEMBER], + acceptedStatuses: [ACCEPTED] + }), + param('organizationId').exists().trim(), + body('success_url').exists().isString(), + body('cancel_url').exists().isString(), + validateRequest, + organizationsController.addOrganizationPmtMethod +); + +router.delete( + '/:organizationId/billing-details/payment-methods/:pmtMethodId', + requireAuth({ + acceptedAuthModes: ['jwt', 'apiKey'] + }), + requireOrganizationAuth({ + acceptedRoles: [OWNER, ADMIN, MEMBER], + acceptedStatuses: [ACCEPTED] + }), + param('organizationId').exists().trim(), + validateRequest, + organizationsController.deleteOrganizationPmtMethod +); + export default router; \ No newline at end of file diff --git a/backend/src/index.ts b/backend/src/index.ts index c4ba7c846..5906cd5a1 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -26,7 +26,8 @@ import { secret as eeSecretRouter, secretSnapshot as eeSecretSnapshotRouter, action as eeActionRouter, - organizations as eeOrganizationsRouter + organizations as eeOrganizationsRouter, + cloudProducts as eeCloudProductsRouter } from './ee/routes/v1'; import { signup as v1SignupRouter, @@ -122,6 +123,7 @@ const main = async () => { app.use('/api/v1/workspace', eeWorkspaceRouter); app.use('/api/v1/action', eeActionRouter); app.use('/api/v1/organizations', eeOrganizationsRouter); + app.use('/api/v1/cloud-products', eeCloudProductsRouter); // v1 routes (default) app.use('/api/v1/signup', v1SignupRouter);