billing - settings - BillingSettingsStorage
MODIFY
src/server/rest/v1/router/api/BillingSettingRouter.ts
src/server/rest/v1/router/api/BillingSettingRouter.ts
@@ -47,7 +47,7 @@ export default class SettingRouter {
}
protected buildRouteActivateBillingSetting(): void {
- this.router.post(`/${ServerRoute.REST_BILLING_SETTING_CHECK}`, (req: Request, res: Response, next: NextFunction) => {
+ this.router.post(`/${ServerRoute.REST_BILLING_SETTING_ACTIVATE}`, (req: Request, res: Response, next: NextFunction) => {
void RouterUtils.handleServerAction(BillingSettingService.handleActivateBillingSetting.bind(this), ServerAction.SETTINGS, req, res, next);
});
}

MODIFY
src/server/rest/v1/service/BillingSettingService.ts
src/server/rest/v1/service/BillingSettingService.ts
import { Action, Entity } from '../../../../types/Authorization';
+import { BillingSettings, SettingDB } from '../../../../types/Setting';
+import { HTTPAuthError, HTTPError } from '../../../../types/HTTPError';
import { NextFunction, Request, Response } from 'express';
import AppAuthError from '../../../../exception/AppAuthError';
+import AppError from '../../../../exception/AppError';
import Authorizations from '../../../../authorization/Authorizations';
import BillingSettingStorage from '../../../../storage/mongodb/BillingSettingStorage';
-import { BillingSettings } from '../../../../types/Setting';
import Constants from '../../../../utils/Constants';
-import { HTTPAuthError } from '../../../../types/HTTPError';
+import Cypher from '../../../../utils/Cypher';
import { ServerAction } from '../../../../types/Server';
+import SettingSecurity from './security/SettingSecurity';
import { StatusCodes } from 'http-status-codes';
+import UtilsService from './UtilsService';
+import _ from 'lodash';
const MODULE_NAME = 'BillingSettingService';
@@ -52,6 +57,13 @@ export default class BillingSettingService {
}
public static async handleUpdateBillingSetting(action: ServerAction, req: Request, res: Response, next: NextFunction): Promise<void> {
+ // Filter
+ // TODO - should only accept BillingSettings as input
+ // const newSettings = SettingSecurity.filterBillingSettingUpdateRequest(req);
+ // TODO - sanitize it
+ const settingID = req.params.id;
+ const newSettings = req.body;
+ UtilsService.assertIdIsProvided(action, newSettings.id, MODULE_NAME, 'handleUpdateSetting', req.user);
if (!await Authorizations.canUpdateSetting(req.user)) {
throw new AppAuthError({
errorCode: HTTPAuthError.FORBIDDEN,
@@ -60,11 +72,16 @@ export default class BillingSettingService {
module: MODULE_NAME, method: 'handleUpdateBillingSetting',
});
}
- res.sendStatus(StatusCodes.NOT_IMPLEMENTED);
- // const billingSettings: BillingSettings = null;
- // await BillingSettingStorage.saveBillingSettings(req.user.tenantID, billingSettings);
- // Ok
- // res.json(Constants.REST_RESPONSE_SUCCESS);
+ // Update timestamp
+ newSettings.lastChangedBy = { 'id': req.user.id };
+ newSettings.lastChangedOn = new Date();
+ // Let's save it
+ const id = await BillingSettingStorage.saveBillingSetting(req.user.tenantID, settingID, newSettings as BillingSettings, true);
+ if (!id) {
+ res.sendStatus(StatusCodes.NOT_FOUND);
+ } else {
+ res.json(Constants.REST_RESPONSE_SUCCESS);
+ }
next();
}
@@ -80,4 +97,48 @@ export default class BillingSettingService {
next();
}
+ private static async processSensitiveDataCallback(req: Request, setting: SettingDB, settingUpdate: Partial<BillingSettings>) {
+ // Process the sensitive data if any
+ // Preprocess the data to take care of updated values
+ if (settingUpdate.sensitiveData) {
+ if (!Array.isArray(settingUpdate.sensitiveData)) {
+ throw new AppError({
+ source: Constants.CENTRAL_SERVER,
+ errorCode: HTTPError.CYPHER_INVALID_SENSITIVE_DATA_ERROR,
+ message: `The property 'sensitiveData' for Setting with ID '${settingUpdate.id}' is not an array`,
+ module: MODULE_NAME,
+ method: 'handleUpdateSetting',
+ user: req.user
+ });
+ }
+ // Process sensitive properties
+ for (const property of settingUpdate.sensitiveData) {
+ // Get the sensitive property from the request
+ const valueInRequest = _.get(settingUpdate, property);
+ if (valueInRequest && valueInRequest.length > 0) {
+ // Get the sensitive property from the DB
+ const valueInDb = _.get(setting, property);
+ if (valueInDb && valueInDb.length > 0) {
+ const hashedValueInDB = Cypher.hash(valueInDb);
+ if (valueInRequest !== hashedValueInDB) {
+ // Yes: Encrypt
+ _.set(settingUpdate, property, await Cypher.encrypt(req.user.tenantID, valueInRequest));
+ } else {
+ // No: Put back the encrypted value
+ _.set(settingUpdate, property, valueInDb);
+ }
+ } else {
+ // Value in db is empty then encrypt
+ _.set(settingUpdate, property, await Cypher.encrypt(req.user.tenantID, valueInRequest));
+ }
+ }
+ }
+ } else {
+ settingUpdate.sensitiveData = [];
+ }
+ // Update timestamp
+ setting.lastChangedBy = { 'id': req.user.id };
+ setting.lastChangedOn = new Date();
+ }
+
}

MODIFY
src/server/rest/v1/service/security/SettingSecurity.ts
src/server/rest/v1/service/security/SettingSecurity.ts
@@ -37,6 +37,12 @@ export default class SettingSecurity {
return SettingSecurity._filterSettingRequest(request);
}
+ public static filterBillingSettingUpdateRequest(request: any): Partial<SettingDB> {
+ const filteredRequest = SettingSecurity._filterSettingRequest(request.body);
+ filteredRequest.id = sanitize(request.params.id);
+ return filteredRequest;
+ }
+
private static _filterSettingRequest(request: Partial<SettingDB>): Partial<SettingDB> {
const settings: SettingDB = {
identifier: sanitize(request.identifier),

MODIFY
src/storage/mongodb/BillingSettingStorage.ts
src/storage/mongodb/BillingSettingStorage.ts
import { BillingSettings, BillingSettingsType, SettingDB } from '../../types/Setting';
-import BackendError from '../../exception/BackendError';
+import AppError from '../../exception/AppError';
import Constants from '../../utils/Constants';
import Cypher from '../../utils/Cypher';
+import { HTTPError } from '../../types/HTTPError';
import SettingStorage from './SettingStorage';
import TenantComponents from '../../types/TenantComponents';
+import _ from 'lodash';
const MODULE_NAME = 'BillingSettingStorage';
export default class BillingSettingStorage {
@@ -15,13 +17,13 @@ export default class BillingSettingStorage {
Cypher.hashSensitiveDataInJSON(setting);
}
// Extract the data
- const config = setting.content;
+ const content = setting.content;
billingSettings.id = setting.id;
billingSettings.sensitiveData = setting.sensitiveData;
billingSettings.backupSensitiveData = setting.backupSensitiveData;
// Billing Common Properties
- if (config.billing) {
- const { isTransactionBillingActivated, immediateBillingAllowed, periodicBillingAllowed, taxID, usersLastSynchronizedOn } = config.billing;
+ if (content.billing) {
+ const { isTransactionBillingActivated, immediateBillingAllowed, periodicBillingAllowed, taxID, usersLastSynchronizedOn } = content.billing;
billingSettings.billing = {
isTransactionBillingActivated,
immediateBillingAllowed,
@@ -31,9 +33,9 @@ export default class BillingSettingStorage {
};
}
// Billing Concrete Implementation Properties
- if (config.stripe) {
+ if (content.stripe) {
billingSettings.type = BillingSettingsType.STRIPE;
- const { url, publicKey, secretKey } = config.stripe;
+ const { url, publicKey, secretKey } = content.stripe;
billingSettings.stripe = {
url,
publicKey,
@@ -67,36 +69,93 @@ export default class BillingSettingStorage {
return null;
}
- public static async saveBillingSettings(tenantID: string, billingSettingsToSave: BillingSettings): Promise<string> {
+ public static async saveBillingSetting(tenantID: string, settingID: string, newSettings: Partial<BillingSettings>, encryptSensitiveData = false): Promise<string> {
// Load previous settings
- const settings = await SettingStorage.getBillingSettings(tenantID);
+ const settings = await SettingStorage.getSettings(tenantID, { settingID }, Constants.DB_PARAMS_SINGLE_RECORD);
+ if (settings?.result?.[0]?.content) {
// Build internal structure
- const settingsToSave = {
- id: billingSettingsToSave.id,
- identifier: billingSettingsToSave.identifier,
- sensitiveData: billingSettingsToSave.sensitiveData,
- backupSensitiveData: billingSettingsToSave.backupSensitiveData,
- lastChangedOn: new Date(),
+ const settingsToSave = settings.result[0];
+ if (encryptSensitiveData) {
+ // TODO - MUST BE FIRST CONVERTED to the SETTING format
+ await BillingSettingStorage.processSensitiveData(tenantID, settingsToSave, newSettings);
+ }
+ const { isTransactionBillingActivated, usersLastSynchronizedOn } = settingsToSave.content.billing;
+ const { immediateBillingAllowed, periodicBillingAllowed, taxID } = newSettings.billing;
+ // Common Properties
+ settingsToSave.content.billing = {
+ isTransactionBillingActivated,usersLastSynchronizedOn,
+ immediateBillingAllowed, periodicBillingAllowed, taxID
+ };
+ // STRIPE Properties
+ if (settingsToSave.content.type === BillingSettingsType.STRIPE) {
+ settingsToSave.content.stripe = newSettings.stripe;
+ }
+ // Save
+ return SettingStorage.saveSettings(tenantID, settingsToSave);
+ }
+ return null;
+ }
+
+ private static async processSensitiveData(tenantID: string, setting: SettingDB, billingSettings: Partial<BillingSettings>) {
+ const { identifier, type, sensitiveData, billing, stripe } = billingSettings;
+ const settingUpdate: SettingDB = {
+ identifier,
+ sensitiveData,
content: {
- billing: billingSettingsToSave.billing,
- stripe: billingSettingsToSave.stripe
- },
- } as SettingDB;
- // Preserve Data that the DASHBOARD is not aware off
- settingsToSave.content.billing.isTransactionBillingActivated = settings.billing.isTransactionBillingActivated;
- // Populates implementation-specific data
- if (settings.type === BillingSettingsType.STRIPE) {
- if (!billingSettingsToSave.stripe.secretKey) {
- throw new BackendError({
+ type,
+ billing,
+ stripe
+ }
+ };
+ await BillingSettingStorage._processSensitiveData(tenantID, setting, settingUpdate);
+ }
+
+ private static async _processSensitiveData(tenantID: string, setting: SettingDB, settingUpdate: SettingDB) {
+ // Process the sensitive data if any
+ // Preprocess the data to take care of updated values
+ if (settingUpdate.sensitiveData) {
+ if (!Array.isArray(settingUpdate.sensitiveData)) {
+ throw new AppError({
source: Constants.CENTRAL_SERVER,
+ errorCode: HTTPError.CYPHER_INVALID_SENSITIVE_DATA_ERROR,
+ message: `The property 'sensitiveData' for Setting with ID '${settingUpdate.id}' is not an array`,
module: MODULE_NAME,
- method: 'saveBillingSettings',
- message: 'One or several mandatory fields are missing'
+ method: 'handleUpdateSetting',
});
}
- settingsToSave.content.stripe = billingSettingsToSave.stripe;
+ // Process sensitive properties
+ for (const property of settingUpdate.sensitiveData) {
+ // Get the sensitive property from the request
+ const valueInRequest = _.get(settingUpdate, property);
+ if (valueInRequest && valueInRequest.length > 0) {
+ // Get the sensitive property from the DB
+ const valueInDb = _.get(setting, property);
+ if (valueInDb && valueInDb.length > 0) {
+ const hashedValueInDB = Cypher.hash(valueInDb);
+ if (valueInRequest !== hashedValueInDB) {
+ // Yes: Encrypt
+ _.set(settingUpdate, property, await Cypher.encrypt(tenantID, valueInRequest));
+ } else {
+ // No: Put back the encrypted value
+ _.set(settingUpdate, property, valueInDb);
}
- // Save
- return SettingStorage.saveSettings(tenantID, settingsToSave);
+ } else {
+ // Value in db is empty then encrypt
+ _.set(settingUpdate, property, await Cypher.encrypt(tenantID, valueInRequest));
}
+ } else {
+ throw new AppError({
+ source: Constants.CENTRAL_SERVER,
+ errorCode: HTTPError.CYPHER_INVALID_SENSITIVE_DATA_ERROR,
+ message: `The property '${property}' for Setting with ID '${settingUpdate.id}' is not set`,
+ module: MODULE_NAME,
+ method: 'handleUpdateSetting',
+ });
+ }
+ }
+ } else {
+ settingUpdate.sensitiveData = [];
+ }
+ }
+
}

billing - settings - BillingSettingsStorage