From 5ba73eaf79920d6fd529f08493cba40c3fc7d208 Mon Sep 17 00:00:00 2001 From: Ramzi Siala <1226644+Ramzay@users.noreply.github.com> Date: Fri, 23 Dec 2022 17:22:39 +0100 Subject: [PATCH 01/12] Migrate statistic to abac - Handle retro compatibility - Use new statistic entity / authorizations - Add new flag to return stat datasource + auth: default false - Simple statistic authorization: one action only - Statistic data is not typed --- .vscode/settings-template.json | 2 +- .../v1/schemas/statistic/statistics-get.json | 4 + src/authorization/AuthorizationsDefinition.ts | 100 +++++ .../rest/v1/service/AuthorizationService.ts | 33 +- .../rest/v1/service/StatisticService.ts | 347 ++++++++---------- src/server/rest/v1/service/UtilsService.ts | 5 + src/types/Authorization.ts | 3 +- src/types/DataResult.ts | 8 + src/types/requests/HttpStatisticRequest.ts | 1 + 9 files changed, 310 insertions(+), 193 deletions(-) diff --git a/.vscode/settings-template.json b/.vscode/settings-template.json index 5c32703620..3d8a764174 100644 --- a/.vscode/settings-template.json +++ b/.vscode/settings-template.json @@ -9,6 +9,6 @@ "typescriptreact" ], "jest.jestCommandLine": "npm run test -- ", - "jest.autoRun": "false" + "jest.autoRun": { "watch": "false" } } \ No newline at end of file diff --git a/src/assets/server/rest/v1/schemas/statistic/statistics-get.json b/src/assets/server/rest/v1/schemas/statistic/statistics-get.json index f1b2eeb7ce..7a50e9fb87 100644 --- a/src/assets/server/rest/v1/schemas/statistic/statistics-get.json +++ b/src/assets/server/rest/v1/schemas/statistic/statistics-get.json @@ -32,6 +32,10 @@ "SiteID": { "$ref": "common#/definitions/ids" }, + "WithAuth": { + "$ref": "common#/definitions/withAuth", + "default": false + }, "DataScope": { "type": "string", "enum": [ diff --git a/src/authorization/AuthorizationsDefinition.ts b/src/authorization/AuthorizationsDefinition.ts index 1e3b0e9e57..5c5c49d0e9 100644 --- a/src/authorization/AuthorizationsDefinition.ts +++ b/src/authorization/AuthorizationsDefinition.ts @@ -130,6 +130,12 @@ export const AUTHORIZATION_DEFINITION: AuthorizationDefinition = { 'miscSegment', 'miscIsofixSeats', 'chargeStandardPower', 'chargeStandardPhase', 'hash', 'image' ] }, + { + resource: Entity.STATISTIC, action: Action.READ, + }, + { + resource: Entity.STATISTIC, action: Action.EXPORT, + }, ] }, admin: { @@ -552,6 +558,12 @@ export const AUTHORIZATION_DEFINITION: AuthorizationDefinition = { } }, }, + { + resource: Entity.STATISTIC, action: Action.READ, + }, + { + resource: Entity.STATISTIC, action: Action.EXPORT, + }, { resource: Entity.TRANSACTION, action: Action.LIST, condition: { @@ -1516,6 +1528,28 @@ export const AUTHORIZATION_DEFINITION: AuthorizationDefinition = { 'user.name', 'user.firstName', 'user.email', 'createdOn', 'lastChangedOn' ] }, + { + resource: Entity.STATISTIC, action: Action.READ, + condition: { + Fn: 'custom:dynamicAuthorizations', + args: { + asserts: [], + filters: ['OwnUser'] + } + }, + attributes: ['*'] + }, + { + resource: Entity.STATISTIC, action: Action.EXPORT, + condition: { + Fn: 'custom:dynamicAuthorizations', + args: { + asserts: [], + filters: ['OwnUser'] + } + }, + attributes: ['*'] + }, { resource: Entity.TRANSACTION, action: Action.LIST, condition: { @@ -1923,6 +1957,28 @@ export const AUTHORIZATION_DEFINITION: AuthorizationDefinition = { } }, }, + { + resource: Entity.STATISTIC, action: Action.READ, + condition: { + Fn: 'custom:dynamicAuthorizations', + args: { + asserts: [], + filters: ['AssignedSites'] + } + }, + attributes: ['*'] + }, + { + resource: Entity.STATISTIC, action: Action.EXPORT, + condition: { + Fn: 'custom:dynamicAuthorizations', + args: { + asserts: [], + filters: ['AssignedSites'] + } + }, + attributes: ['*'] + }, { resource: Entity.TRANSACTION, action: Action.LIST, condition: { @@ -2628,6 +2684,28 @@ export const AUTHORIZATION_DEFINITION: AuthorizationDefinition = { } }, }, + { + resource: Entity.STATISTIC, action: Action.READ, + condition: { + Fn: 'custom:dynamicAuthorizations', + args: { + asserts: [], + filters: ['SitesAdmin'] + } + }, + attributes: ['*'] + }, + { + resource: Entity.STATISTIC, action: Action.EXPORT, + condition: { + Fn: 'custom:dynamicAuthorizations', + args: { + asserts: [], + filters: ['SitesAdmin'] + } + }, + attributes: ['*'] + }, { resource: Entity.TRANSACTION, action: Action.LIST, condition: { @@ -3116,6 +3194,28 @@ export const AUTHORIZATION_DEFINITION: AuthorizationDefinition = { 'user.id', 'user.name', 'user.firstName', 'user.email', 'user.role', 'siteID', 'siteAdmin', 'siteOwner', ] }, + { + resource: Entity.STATISTIC, action: Action.READ, + condition: { + Fn: 'custom:dynamicAuthorizations', + args: { + asserts: [], + filters: ['SitesOwner'] + } + }, + attributes: ['*'] + }, + { + resource: Entity.STATISTIC, action: Action.EXPORT, + condition: { + Fn: 'custom:dynamicAuthorizations', + args: { + asserts: [], + filters: ['SitesOwner'] + } + }, + attributes: ['*'] + }, { resource: Entity.TRANSACTION, action: Action.READ, condition: { diff --git a/src/server/rest/v1/service/AuthorizationService.ts b/src/server/rest/v1/service/AuthorizationService.ts index 61ed83da17..32c07c1d54 100644 --- a/src/server/rest/v1/service/AuthorizationService.ts +++ b/src/server/rest/v1/service/AuthorizationService.ts @@ -1,5 +1,5 @@ import { Action, AuthorizationActions, AuthorizationContext, AuthorizationFilter, DynamicAuthorizationsFilter, Entity } from '../../../../types/Authorization'; -import { AssetDataResult, BillingAccountsDataResult, BillingInvoiceDataResult, BillingPaymentMethodDataResult, BillingTaxDataResult, BillingTransfersDataResult, CarCatalogDataResult, CarDataResult, ChargingProfileDataResult, ChargingStationDataResult, ChargingStationTemplateDataResult, CompanyDataResult, LogDataResult, OcpiEndpointDataResult, PricingDefinitionDataResult, RegistrationTokenDataResult, SettingDBDataResult, SiteAreaDataResult, SiteDataResult, SiteUserDataResult, TagDataResult, TransactionDataResult, TransactionInErrorDataResult, UserDataResult, UserSiteDataResult } from '../../../../types/DataResult'; +import { AssetDataResult, BillingAccountsDataResult, BillingInvoiceDataResult, BillingPaymentMethodDataResult, BillingTaxDataResult, BillingTransfersDataResult, CarCatalogDataResult, CarDataResult, ChargingProfileDataResult, ChargingStationDataResult, ChargingStationTemplateDataResult, CompanyDataResult, LogDataResult, OcpiEndpointDataResult, PricingDefinitionDataResult, RegistrationTokenDataResult, SettingDBDataResult, SiteAreaDataResult, SiteDataResult, SiteUserDataResult, StatisticDataResult, TagDataResult, TransactionDataResult, TransactionInErrorDataResult, UserDataResult, UserSiteDataResult } from '../../../../types/DataResult'; import { BillingAccount, BillingInvoice, BillingPaymentMethod, BillingTax, BillingTransfer } from '../../../../types/Billing'; import { Car, CarCatalog } from '../../../../types/Car'; import { ChargePointStatus, OCPPProtocol, OCPPVersion } from '../../../../types/ocpp/OCPPServer'; @@ -31,6 +31,7 @@ import HttpByIDRequest from '../../../../types/requests/HttpByIDRequest'; import { HttpLogGetRequest } from '../../../../types/requests/HttpLogRequest'; import { HttpOCPIEndpointGetRequest } from '../../../../types/requests/HttpOCPIEndpointRequest'; import { HttpRegistrationTokenGetRequest } from '../../../../types/requests/HttpRegistrationToken'; +import HttpStatisticsGetRequest from '../../../../types/requests/HttpStatisticRequest'; import { Log } from '../../../../types/Log'; import { OCPICapability } from '../../../../types/ocpi/OCPIEvse'; import OCPIEndpoint from '../../../../types/ocpi/OCPIEndpoint'; @@ -1485,6 +1486,36 @@ export default class AuthorizationService { return authorizations; } + public static async checkAndGetStatisticsAuthorizations(tenant: Tenant, userToken: UserToken, authAction: Action, + filteredRequest?: Partial, failsWithException = true): Promise { + const authorizations: AuthorizationFilter = { + filters: {}, + dataSources: new Map(), + projectFields: [], + authorized: false + }; + // Check static & dynamic authorization + await AuthorizationService.canPerformAuthorizationAction( + tenant, userToken, Entity.STATISTIC, authAction, authorizations, filteredRequest, null, failsWithException); + return authorizations; + } + + // Do not re-use this function for other authorizations, special case below for statistics + public static async addStatisticsAuthorizations(tenant: Tenant, userToken: UserToken, + statistics: StatisticDataResult, authorizationFilter: AuthorizationFilter): Promise { + // Add Authorizations + statistics.canListUsers = await AuthorizationService.canPerformAuthorizationAction( + tenant, userToken, Entity.USER, Action.LIST, authorizationFilter); + statistics.canListChargingStations = await AuthorizationService.canPerformAuthorizationAction( + tenant, userToken, Entity.CHARGING_STATION, Action.LIST, authorizationFilter); + statistics.canListSites = await AuthorizationService.canPerformAuthorizationAction( + tenant, userToken, Entity.SITE, Action.LIST, authorizationFilter); + statistics.canListSiteAreas = await AuthorizationService.canPerformAuthorizationAction( + tenant, userToken, Entity.SITE_AREA, Action.LIST, authorizationFilter); + statistics.canExport = await AuthorizationService.canPerformAuthorizationAction( + tenant, userToken, Entity.STATISTIC, Action.EXPORT, authorizationFilter); + } + private static filterProjectFields(authFields: string[], httpProjectField: string): string[] { // Init with authorization fields let applicableProjectedFields = authFields; diff --git a/src/server/rest/v1/service/StatisticService.ts b/src/server/rest/v1/service/StatisticService.ts index db9e59446e..434259846f 100644 --- a/src/server/rest/v1/service/StatisticService.ts +++ b/src/server/rest/v1/service/StatisticService.ts @@ -1,17 +1,16 @@ /* eslint-disable */ -import { Action, Entity } from '../../../../types/Authorization'; +import { Action, AuthorizationFilter, Entity } from '../../../../types/Authorization'; import { NextFunction, Request, Response } from 'express'; import StatisticFilter, { ChargingStationStats, StatsDataCategory, StatsDataScope, StatsDataType, StatsGroupBy, UserStats } from '../../../../types/Statistic'; +import Tenant, { TenantComponents } from '../../../../types/Tenant'; -import AppAuthError from '../../../../exception/AppAuthError'; -import Authorizations from '../../../../authorization/Authorizations'; +import AuthorizationService from './AuthorizationService'; import Constants from '../../../../utils/Constants'; -import { HTTPAuthError } from '../../../../types/HTTPError'; import HttpStatisticsGetRequest from '../../../../types/requests/HttpStatisticRequest'; import { ServerAction } from '../../../../types/Server'; +import { StatisticDataResult } from '../../../../types/DataResult'; import StatisticsStorage from '../../../../storage/mongodb/StatisticsStorage'; import StatisticsValidatorRest from '../validator/StatisticsValidatorRest'; -import { TenantComponents } from '../../../../types/Tenant'; import UserToken from '../../../../types/UserToken'; import Utils from '../../../../utils/Utils'; import UtilsService from './UtilsService'; @@ -23,310 +22,253 @@ export default class StatisticService { static async handleGetChargingStationConsumptionStatistics(action: ServerAction, req: Request, res: Response, next: NextFunction): Promise { // Check if component is active UtilsService.assertComponentIsActiveFromToken(req.user, TenantComponents.STATISTICS, - Action.LIST, Entity.TRANSACTION, MODULE_NAME, 'handleGetChargingStationConsumptionStatistics'); - // Check auth - if (!await Authorizations.canListTransactions(req.user)) { - throw new AppAuthError({ - errorCode: HTTPAuthError.FORBIDDEN, - user: req.user, - action: Action.LIST, entity: Entity.TRANSACTION, - module: MODULE_NAME, method: 'handleGetChargingStationConsumptionStatistics' - }); - } + Action.READ, Entity.STATISTIC, MODULE_NAME, 'handleGetChargingStationConsumptionStatistics'); // Filter const filteredRequest = StatisticsValidatorRest.getInstance().validateStatisticsGet(req.query); + // Check auth + const authorizations = await AuthorizationService.checkAndGetStatisticsAuthorizations(req.tenant, req.user, Action.READ, filteredRequest); + if (!authorizations.authorized) { + StatisticService.buildAndReturnEmptyStatisticData(res, filteredRequest, next); + return; + } // Build filter - const filter = StatisticService.buildFilter(filteredRequest, req.user); + const filter = await StatisticService.buildFilter(filteredRequest, req.tenant, req.user, authorizations); // Get Stats - const transactionStats = await StatisticsStorage.getChargingStationStats( - req.tenant, filter, StatsGroupBy.CONSUMPTION); + const transactionStats = await StatisticsStorage.getChargingStationStats(req.tenant, filter, StatsGroupBy.CONSUMPTION); // Convert - const transactions = StatisticService.convertToGraphData( - transactionStats, StatsDataCategory.CHARGING_STATION, filter.dataScope); - res.json(transactions); - next(); + const transactions = StatisticService.convertToGraphData(transactionStats, StatsDataCategory.CHARGING_STATION, filter.dataScope); + // Return data + await StatisticService.buildAndReturnStatisticData(req, res, transactions, filteredRequest, authorizations, next); } static async handleGetChargingStationUsageStatistics(action: ServerAction, req: Request, res: Response, next: NextFunction): Promise { // Check if component is active UtilsService.assertComponentIsActiveFromToken(req.user, TenantComponents.STATISTICS, - Action.LIST, Entity.TRANSACTION, MODULE_NAME, 'handleGetChargingStationUsageStatistics'); - // Check auth - if (!await Authorizations.canListTransactions(req.user)) { - throw new AppAuthError({ - errorCode: HTTPAuthError.FORBIDDEN, - user: req.user, - action: Action.LIST, - entity: Entity.TRANSACTION, - module: MODULE_NAME, - method: 'handleGetChargingStationUsageStatistics' - }); - } + Action.READ, Entity.STATISTIC, MODULE_NAME, 'handleGetChargingStationUsageStatistics'); // Filter const filteredRequest = StatisticsValidatorRest.getInstance().validateStatisticsGet(req.query); + // Check auth + const authorizations = await AuthorizationService.checkAndGetStatisticsAuthorizations(req.tenant, req.user, Action.READ, filteredRequest); + if (!authorizations.authorized) { + StatisticService.buildAndReturnEmptyStatisticData(res, filteredRequest, next); + return; + } // Build filter - const filter = StatisticService.buildFilter(filteredRequest, req.user); + const filter = await StatisticService.buildFilter(filteredRequest, req.tenant, req.user, authorizations); // Get Stats const transactionStats = await StatisticsStorage.getChargingStationStats( req.tenant, filter, StatsGroupBy.USAGE); // Convert - const transactions = StatisticService.convertToGraphData( - transactionStats, StatsDataCategory.CHARGING_STATION, filter.dataScope); - res.json(transactions); - next(); + const transactions = StatisticService.convertToGraphData(transactionStats, StatsDataCategory.CHARGING_STATION, filter.dataScope); + // Return data + await StatisticService.buildAndReturnStatisticData(req, res, transactions, filteredRequest, authorizations, next); } static async handleGetChargingStationInactivityStatistics(action: ServerAction, req: Request, res: Response, next: NextFunction): Promise { // Check if component is active UtilsService.assertComponentIsActiveFromToken(req.user, TenantComponents.STATISTICS, - Action.LIST, Entity.TRANSACTION, MODULE_NAME, 'handleGetChargingStationInactivityStatistics'); - // Check auth - if (!await Authorizations.canListTransactions(req.user)) { - throw new AppAuthError({ - errorCode: HTTPAuthError.FORBIDDEN, - user: req.user, - action: Action.LIST, - entity: Entity.TRANSACTION, - module: MODULE_NAME, - method: 'handleGetChargingStationInactivityStatistics' - }); - } + Action.READ, Entity.STATISTIC, MODULE_NAME, 'handleGetChargingStationInactivityStatistics'); // Filter const filteredRequest = StatisticsValidatorRest.getInstance().validateStatisticsGet(req.query); + // Check auth + const authorizations = await AuthorizationService.checkAndGetStatisticsAuthorizations(req.tenant, req.user, Action.READ, filteredRequest); + if (!authorizations.authorized) { + StatisticService.buildAndReturnEmptyStatisticData(res, filteredRequest, next); + return; + } // Build filter - const filter = StatisticService.buildFilter(filteredRequest, req.user); + const filter = await StatisticService.buildFilter(filteredRequest, req.tenant, req.user, authorizations); // Get Stats const transactionStats = await StatisticsStorage.getChargingStationStats( req.tenant, filter, StatsGroupBy.INACTIVITY); // Convert - const transactions = StatisticService.convertToGraphData( - transactionStats, StatsDataCategory.CHARGING_STATION, filter.dataScope); - res.json(transactions); - next(); + const transactions = StatisticService.convertToGraphData(transactionStats, StatsDataCategory.CHARGING_STATION, filter.dataScope); + // Return data + await StatisticService.buildAndReturnStatisticData(req, res, transactions, filteredRequest, authorizations, next); } static async handleGetChargingStationTransactionsStatistics(action: ServerAction, req: Request, res: Response, next: NextFunction): Promise { // Check if component is active UtilsService.assertComponentIsActiveFromToken(req.user, TenantComponents.STATISTICS, - Action.LIST, Entity.TRANSACTION, MODULE_NAME, 'handleGetChargingStationTransactionsStatistics'); - // Check auth - if (!await Authorizations.canListTransactions(req.user)) { - throw new AppAuthError({ - errorCode: HTTPAuthError.FORBIDDEN, - user: req.user, - action: Action.LIST, - entity: Entity.TRANSACTION, - module: MODULE_NAME, - method: 'handleGetChargingStationTransactionsStatistics' - }); - } + Action.READ, Entity.STATISTIC, MODULE_NAME, 'handleGetChargingStationTransactionsStatistics'); // Filter const filteredRequest = StatisticsValidatorRest.getInstance().validateStatisticsGet(req.query); + // Check auth + const authorizations = await AuthorizationService.checkAndGetStatisticsAuthorizations(req.tenant, req.user, Action.READ, filteredRequest); + if (!authorizations.authorized) { + StatisticService.buildAndReturnEmptyStatisticData(res, filteredRequest, next); + return; + } // Build filter - const filter = StatisticService.buildFilter(filteredRequest, req.user); + const filter = await StatisticService.buildFilter(filteredRequest, req.tenant, req.user, authorizations); // Get Stats const transactionStats = await StatisticsStorage.getChargingStationStats( req.tenant, filter, StatsGroupBy.TRANSACTIONS); // Convert const transactions = StatisticService.convertToGraphData( transactionStats, StatsDataCategory.CHARGING_STATION, filter.dataScope); - res.json(transactions); - next(); + // Return data + await StatisticService.buildAndReturnStatisticData(req, res, transactions, filteredRequest, authorizations, next); } static async handleGetChargingStationPricingStatistics(action: ServerAction, req: Request, res: Response, next: NextFunction): Promise { // Check if component is active UtilsService.assertComponentIsActiveFromToken(req.user, TenantComponents.STATISTICS, - Action.LIST, Entity.TRANSACTION, MODULE_NAME, 'handleGetChargingStationPricingStatistics'); - // Check auth - if (!await Authorizations.canListTransactions(req.user)) { - throw new AppAuthError({ - errorCode: HTTPAuthError.FORBIDDEN, - user: req.user, - action: Action.LIST, - entity: Entity.TRANSACTION, - module: MODULE_NAME, - method: 'handleGetChargingStationPricingStatistics' - }); - } + Action.READ, Entity.STATISTIC, MODULE_NAME, 'handleGetChargingStationPricingStatistics'); // Filter const filteredRequest = StatisticsValidatorRest.getInstance().validateStatisticsGet(req.query); + // Check auth + const authorizations = await AuthorizationService.checkAndGetStatisticsAuthorizations(req.tenant, req.user, Action.READ, filteredRequest); + if (!authorizations.authorized) { + StatisticService.buildAndReturnEmptyStatisticData(res, filteredRequest, next); + return; + } // Build filter - const filter = StatisticService.buildFilter(filteredRequest, req.user); + const filter = await StatisticService.buildFilter(filteredRequest, req.tenant, req.user, authorizations); // Get Stats const transactionStats = await StatisticsStorage.getChargingStationStats( req.tenant, filter, StatsGroupBy.PRICING); // Convert const transactions = StatisticService.convertToGraphData( transactionStats, StatsDataCategory.CHARGING_STATION, filter.dataScope); - res.json(transactions); - next(); + // Return data + await StatisticService.buildAndReturnStatisticData(req, res, transactions, filteredRequest, authorizations, next); } static async handleGetUserConsumptionStatistics(action: ServerAction, req: Request, res: Response, next: NextFunction): Promise { // Check if component is active UtilsService.assertComponentIsActiveFromToken(req.user, TenantComponents.STATISTICS, - Action.LIST, Entity.TRANSACTION, MODULE_NAME, 'handleGetUserConsumptionStatistics'); - // Check auth - if (!await Authorizations.canListTransactions(req.user)) { - throw new AppAuthError({ - errorCode: HTTPAuthError.FORBIDDEN, - user: req.user, - action: Action.LIST, - entity: Entity.TRANSACTION, - module: MODULE_NAME, - method: 'handleGetUserConsumptionStatistics' - }); - } + Action.READ, Entity.STATISTIC, MODULE_NAME, 'handleGetUserConsumptionStatistics'); // Filter const filteredRequest = StatisticsValidatorRest.getInstance().validateStatisticsGet(req.query); + // Check auth + const authorizations = await AuthorizationService.checkAndGetStatisticsAuthorizations(req.tenant, req.user, Action.READ, filteredRequest); + if (!authorizations.authorized) { + StatisticService.buildAndReturnEmptyStatisticData(res, filteredRequest, next); + return; + } // Build filter - const filter = StatisticService.buildFilter(filteredRequest, req.user); + const filter = await StatisticService.buildFilter(filteredRequest, req.tenant, req.user, authorizations); // Get Stats const transactionStats = await StatisticsStorage.getUserStats( req.tenant, filter, StatsGroupBy.CONSUMPTION); // Convert const transactions = StatisticService.convertToGraphData( transactionStats, StatsDataCategory.USER); - res.json(transactions); - next(); + // Return data + await StatisticService.buildAndReturnStatisticData(req, res, transactions, filteredRequest, authorizations, next); } static async handleGetUserUsageStatistics(action: ServerAction, req: Request, res: Response, next: NextFunction): Promise { // Check if component is active UtilsService.assertComponentIsActiveFromToken(req.user, TenantComponents.STATISTICS, - Action.LIST, Entity.TRANSACTION, MODULE_NAME, 'handleGetUserUsageStatistics'); - // Check auth - if (!await Authorizations.canListTransactions(req.user)) { - throw new AppAuthError({ - errorCode: HTTPAuthError.FORBIDDEN, - user: req.user, - action: Action.LIST, - entity: Entity.TRANSACTION, - module: MODULE_NAME, - method: 'handleGetUserUsageStatistics' - }); - } + Action.READ, Entity.STATISTIC, MODULE_NAME, 'handleGetUserUsageStatistics'); // Filter const filteredRequest = StatisticsValidatorRest.getInstance().validateStatisticsGet(req.query); + // Check auth + const authorizations = await AuthorizationService.checkAndGetStatisticsAuthorizations(req.tenant, req.user, Action.READ, filteredRequest); + if (!authorizations.authorized) { + StatisticService.buildAndReturnEmptyStatisticData(res, filteredRequest, next); + return; + } // Build filter - const filter = StatisticService.buildFilter(filteredRequest, req.user); + const filter = await StatisticService.buildFilter(filteredRequest, req.tenant, req.user, authorizations); // Get Stats const transactionStats = await StatisticsStorage.getUserStats( req.tenant, filter, StatsGroupBy.USAGE); // Convert const transactions = StatisticService.convertToGraphData( transactionStats, StatsDataCategory.USER); - res.json(transactions); - next(); + // Return data + await StatisticService.buildAndReturnStatisticData(req, res, transactions, filteredRequest, authorizations, next); } static async handleGetUserInactivityStatistics(action: ServerAction, req: Request, res: Response, next: NextFunction): Promise { // Check if component is active UtilsService.assertComponentIsActiveFromToken(req.user, TenantComponents.STATISTICS, - Action.LIST, Entity.TRANSACTION, MODULE_NAME, 'handleGetUserInactivityStatistics'); - // Check auth - if (!await Authorizations.canListTransactions(req.user)) { - throw new AppAuthError({ - errorCode: HTTPAuthError.FORBIDDEN, - user: req.user, - action: Action.LIST, - entity: Entity.TRANSACTION, - module: MODULE_NAME, - method: 'handleGetUserInactivityStatistics' - }); - } + Action.READ, Entity.STATISTIC, MODULE_NAME, 'handleGetUserInactivityStatistics'); // Filter const filteredRequest = StatisticsValidatorRest.getInstance().validateStatisticsGet(req.query); + // Check auth + const authorizations = await AuthorizationService.checkAndGetStatisticsAuthorizations(req.tenant, req.user, Action.READ, filteredRequest); + if (!authorizations.authorized) { + StatisticService.buildAndReturnEmptyStatisticData(res, filteredRequest, next); + return; + } // Build filter - const filter = StatisticService.buildFilter(filteredRequest, req.user); + const filter = await StatisticService.buildFilter(filteredRequest, req.tenant, req.user, authorizations); // Get Stats const transactionStats = await StatisticsStorage.getUserStats( req.tenant, filter, StatsGroupBy.INACTIVITY); // Convert const transactions = StatisticService.convertToGraphData( transactionStats, StatsDataCategory.USER); - res.json(transactions); - next(); + // Return data + await StatisticService.buildAndReturnStatisticData(req, res, transactions, filteredRequest, authorizations, next); } static async handleGetUserTransactionsStatistics(action: ServerAction, req: Request, res: Response, next: NextFunction): Promise { // Check if component is active UtilsService.assertComponentIsActiveFromToken(req.user, TenantComponents.STATISTICS, - Action.LIST, Entity.TRANSACTION, MODULE_NAME, 'handleGetUserTransactionsStatistics'); - // Check auth - if (!await Authorizations.canListTransactions(req.user)) { - throw new AppAuthError({ - errorCode: HTTPAuthError.FORBIDDEN, - user: req.user, - action: Action.LIST, - entity: Entity.TRANSACTION, - module: MODULE_NAME, - method: 'handleGetUserTransactionsStatistics' - }); - } + Action.READ, Entity.STATISTIC, MODULE_NAME, 'handleGetUserTransactionsStatistics'); // Filter const filteredRequest = StatisticsValidatorRest.getInstance().validateStatisticsGet(req.query); + // Check auth + const authorizations = await AuthorizationService.checkAndGetStatisticsAuthorizations(req.tenant, req.user, Action.READ, filteredRequest); + if (!authorizations.authorized) { + StatisticService.buildAndReturnEmptyStatisticData(res, filteredRequest, next); + return; + } // Build filter - const filter = StatisticService.buildFilter(filteredRequest, req.user); + const filter = await StatisticService.buildFilter(filteredRequest, req.tenant, req.user, authorizations); // Get Stats const transactionStats = await StatisticsStorage.getUserStats( req.tenant, filter, StatsGroupBy.TRANSACTIONS); // Convert const transactions = StatisticService.convertToGraphData( transactionStats, StatsDataCategory.USER); - res.json(transactions); - next(); + // Return data + await StatisticService.buildAndReturnStatisticData(req, res, transactions, filteredRequest, authorizations, next); } static async handleGetUserPricingStatistics(action: ServerAction, req: Request, res: Response, next: NextFunction): Promise { // Check if component is active UtilsService.assertComponentIsActiveFromToken(req.user, TenantComponents.STATISTICS, - Action.LIST, Entity.TRANSACTION, MODULE_NAME, 'handleGetUserPricingStatistics'); - // Check auth - if (!await Authorizations.canListTransactions(req.user)) { - throw new AppAuthError({ - errorCode: HTTPAuthError.FORBIDDEN, - user: req.user, - action: Action.LIST, - entity: Entity.TRANSACTION, - module: MODULE_NAME, - method: 'handleGetUserPricingStatistics' - }); - } + Action.READ, Entity.STATISTIC, MODULE_NAME, 'handleGetUserPricingStatistics'); // Filter const filteredRequest = StatisticsValidatorRest.getInstance().validateStatisticsGet(req.query); + // Check auth + const authorizations = await AuthorizationService.checkAndGetStatisticsAuthorizations(req.tenant, req.user, Action.READ, filteredRequest); + if (!authorizations.authorized) { + StatisticService.buildAndReturnEmptyStatisticData(res, filteredRequest, next); + return; + } // Build filter - const filter = StatisticService.buildFilter(filteredRequest, req.user); + const filter = await StatisticService.buildFilter(filteredRequest, req.tenant, req.user, authorizations); // Get Stats const transactionStats = await StatisticsStorage.getUserStats( req.tenant, filter, StatsGroupBy.PRICING); // Convert const transactions = StatisticService.convertToGraphData( transactionStats, StatsDataCategory.USER); - res.json(transactions); - next(); + // Return data + await StatisticService.buildAndReturnStatisticData(req, res, transactions, filteredRequest, authorizations, next); } static async handleExportStatistics(action: ServerAction, req: Request, res: Response, next: NextFunction): Promise { // Check if component is active UtilsService.assertComponentIsActiveFromToken(req.user, TenantComponents.STATISTICS, - Action.LIST, Entity.TRANSACTION, MODULE_NAME, 'handleExportStatistics'); - // Check auth - if (!await Authorizations.canListTransactions(req.user)) { - throw new AppAuthError({ - errorCode: HTTPAuthError.FORBIDDEN, - user: req.user, - action: Action.LIST, - entity: Entity.TRANSACTION, - module: MODULE_NAME, - method: 'handleExportStatistics' - }); - } + Action.READ, Entity.STATISTIC, MODULE_NAME, 'handleExportStatistics'); // Filter const filteredRequest = StatisticsValidatorRest.getInstance().validateStatisticsExport(req.query); + // Check auth + const authorizations = await AuthorizationService.checkAndGetStatisticsAuthorizations(req.tenant, req.user, Action.READ, filteredRequest); + if (!authorizations.authorized) { + StatisticService.buildAndReturnEmptyStatisticData(res, filteredRequest, next); + return; + } // Build filter - const filter = StatisticService.buildFilter(filteredRequest, req.user); + const filter = await StatisticService.buildFilter(filteredRequest, req.tenant, req.user, authorizations); // Decisions let groupBy: string; switch (filteredRequest.DataType) { @@ -366,9 +308,9 @@ export default class StatisticService { res.end(); } - // Only completed transactions - static buildFilter(filteredRequest: HttpStatisticsGetRequest, loggedUser: UserToken): StatisticFilter { - const filter: StatisticFilter = { stop: { $exists: true } }; + static async buildFilter(filteredRequest: HttpStatisticsGetRequest, tenant: Tenant, userToken: UserToken, authorizations: AuthorizationFilter): Promise { + // Only completed transactions + let filter: StatisticFilter = { stop: { $exists: true } }; // Date if ('Year' in filteredRequest) { if (filteredRequest.Year > 0) { @@ -407,21 +349,15 @@ export default class StatisticService { filter.dataScope = filteredRequest.DataScope; } // User - if (Authorizations.isBasic(loggedUser)) { - if (Authorizations.isSiteAdmin(loggedUser)) { - if (filteredRequest.UserID) { - filter.userIDs = filteredRequest.UserID.split('|'); - } else { - // Only for current sites - filter.siteIDs = loggedUser.sitesAdmin; - } - } else { - // Only for current user - filter.userIDs = [loggedUser.id]; - } - } else if (!Authorizations.isBasic(loggedUser) && filteredRequest.UserID) { + if (filteredRequest.UserID) { filter.userIDs = filteredRequest.UserID.split('|'); } + // Override filter with authorizations + filter = { ...filter, ...authorizations.filters }; + // Remove site filter in case own user search + if (!filteredRequest.SiteID && filter.userIDs && filter.userIDs.length === 1 && filter.userIDs[0] === userToken.id) { + filter.siteIDs = []; + } return filter; } @@ -664,4 +600,35 @@ export default class StatisticService { return [headers, rows].join(Constants.CR_LF); } } + + // Function that allows retrocompatibility: empty array or empty data source + private static buildAndReturnEmptyStatisticData(res: Response, filteredRequest: HttpStatisticsGetRequest, next: NextFunction) { + // Empty data result + if (filteredRequest.WithAuth) { + UtilsService.sendEmptyDataResult(res, next); + return; + } + // Empty array + UtilsService.sendEmptyArray(res, next); + return; + } + + // Function that allows retrocompatibility: would either return raw statistic values or convert it into a datasource with auth flags + private static async buildAndReturnStatisticData(req: Request, res: Response, data: any, filteredRequest: HttpStatisticsGetRequest, authorizations: AuthorizationFilter, next: NextFunction) { + // Check return type and add auth + if (filteredRequest.WithAuth) { + const transactionsDataResult: StatisticDataResult = { + result: data, + count: data.length + } + // Add auth + await AuthorizationService.addStatisticsAuthorizations(req.tenant, req.user, transactionsDataResult, authorizations); + res.json(transactionsDataResult); + next(); + } + else { + res.json(data); + next(); + } + } } diff --git a/src/server/rest/v1/service/UtilsService.ts b/src/server/rest/v1/service/UtilsService.ts index de6fcbbf88..e8c4780d96 100644 --- a/src/server/rest/v1/service/UtilsService.ts +++ b/src/server/rest/v1/service/UtilsService.ts @@ -1064,6 +1064,11 @@ export default class UtilsService { next(); } + public static sendEmptyArray(res: Response, next: NextFunction): void { + res.json([]); + next(); + } + public static async handleUnknownAction(action: ServerAction, req: Request, res: Response, next: NextFunction): Promise { // Action provided if (!action) { diff --git a/src/types/Authorization.ts b/src/types/Authorization.ts index be7926c42b..018135988b 100644 --- a/src/types/Authorization.ts +++ b/src/types/Authorization.ts @@ -111,7 +111,8 @@ export enum Entity { PAYMENT_METHOD = 'PaymentMethod', SOURCE = 'Source', CONSUMPTION = 'Consumption', - SMART_CHARGING = 'SmartCharging' + SMART_CHARGING = 'SmartCharging', + STATISTIC = 'Statistic' } export enum Action { diff --git a/src/types/DataResult.ts b/src/types/DataResult.ts index b92f869681..d2cbfcee50 100644 --- a/src/types/DataResult.ts +++ b/src/types/DataResult.ts @@ -188,3 +188,11 @@ export interface SettingDBDataResult extends DataResult { export interface OcpiEndpointDataResult extends DataResult { canCreate?: boolean; } + +export interface StatisticDataResult extends DataResult { + canListUsers?: boolean; + canListChargingStations?: boolean; + canListSites?: boolean; + canListSiteAreas?: boolean; + canExport?: boolean; +} diff --git a/src/types/requests/HttpStatisticRequest.ts b/src/types/requests/HttpStatisticRequest.ts index daa9d8b499..c4a60b9ba5 100644 --- a/src/types/requests/HttpStatisticRequest.ts +++ b/src/types/requests/HttpStatisticRequest.ts @@ -16,6 +16,7 @@ export default interface HttpStatisticsGetRequest { DataType?: StatsDataType; DataCategory?: StatsDataCategory; DataScope?: StatsDataScope; + WithAuth?: boolean; } export interface HttpMetricsStatisticsGetRequest { From 3ba442879a1b39a2595f6962d2c3d0174ee301d0 Mon Sep 17 00:00:00 2001 From: Ramzi Siala <1226644+Ramzay@users.noreply.github.com> Date: Tue, 21 Mar 2023 12:13:11 +0100 Subject: [PATCH 02/12] Statistic export uses filter in UI --- .../schemas/statistic/statistics-export.json | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/assets/server/rest/v1/schemas/statistic/statistics-export.json b/src/assets/server/rest/v1/schemas/statistic/statistics-export.json index cb575f6f78..466fc9c6f6 100644 --- a/src/assets/server/rest/v1/schemas/statistic/statistics-export.json +++ b/src/assets/server/rest/v1/schemas/statistic/statistics-export.json @@ -14,6 +14,30 @@ ], "sanitize": "mongo" }, + "StartDateTime": { + "type": "string", + "format": "date-time", + "customType": "date", + "sanitize": "mongo" + }, + "EndDateTime": { + "type": "string", + "format": "date-time", + "customType": "date", + "sanitize": "mongo" + }, + "SiteAreaID": { + "$ref": "common#/definitions/ids" + }, + "ChargingStationID": { + "$ref": "common#/definitions/string-ids" + }, + "UserID": { + "$ref": "common#/definitions/ids" + }, + "SiteID": { + "$ref": "common#/definitions/ids" + }, "DataCategory": { "type": "string", "enum": [ From fecabd69498050547bbb84733b5126f0341ab7b6 Mon Sep 17 00:00:00 2001 From: LucasBrazi06 Date: Thu, 23 Mar 2023 18:06:58 +0100 Subject: [PATCH 03/12] Don't recreate index with partialFilterExpression --- src/utils/Utils.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/utils/Utils.ts b/src/utils/Utils.ts index f5399f9afd..b76aefa553 100644 --- a/src/utils/Utils.ts +++ b/src/utils/Utils.ts @@ -230,12 +230,7 @@ export default class Utils { } public static areObjectPropertiesEqual(objCmp1: any = {}, objCmp2: any = {}, key: string): boolean { - // Check DB expireAfterSeconds index - if ((Utils.objectHasProperty(objCmp1, key) !== Utils.objectHasProperty(objCmp2, key)) || - (objCmp1[key] !== objCmp2[key])) { - return false; - } - return true; + return _.isEqual(objCmp1[key], objCmp2[key]); } public static computeTimeDurationSecs(timeStart: number): number { From 5fd132caf6cb7c97dc8936eb4015c6b9483bbb3d Mon Sep 17 00:00:00 2001 From: LucasBrazi06 Date: Fri, 24 Mar 2023 11:42:24 +0100 Subject: [PATCH 04/12] Default efficiency not used in % --- .../sap-smart-charging/SapSmartChargingIntegration.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/integration/smart-charging/sap-smart-charging/SapSmartChargingIntegration.ts b/src/integration/smart-charging/sap-smart-charging/SapSmartChargingIntegration.ts index ae114dc4e5..ed7b689e3c 100644 --- a/src/integration/smart-charging/sap-smart-charging/SapSmartChargingIntegration.ts +++ b/src/integration/smart-charging/sap-smart-charging/SapSmartChargingIntegration.ts @@ -906,7 +906,7 @@ export default class SapSmartChargingIntegration extends SmartChargingIntegratio if (chargePoint?.efficiency > 0) { return Utils.roundTo(currentLimit * chargePoint.efficiency / 100 * numberOfConnectedPhase, 1); } - return Utils.roundTo(currentLimit * Constants.DC_CHARGING_STATION_DEFAULT_EFFICIENCY_PERCENT * numberOfConnectedPhase, 1); + return Utils.roundTo(currentLimit * Constants.DC_CHARGING_STATION_DEFAULT_EFFICIENCY_PERCENT / 100 * numberOfConnectedPhase, 1); } return Utils.roundTo((currentLimit * numberOfConnectedPhase), 1); } From 8449f924ab5c1199836b828661ac254cedd9e3af Mon Sep 17 00:00:00 2001 From: ClaudeROSSI Date: Sun, 26 Mar 2023 09:14:52 +0200 Subject: [PATCH 05/12] tenant - optional cpmsDomainName --- src/types/Tenant.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/types/Tenant.ts b/src/types/Tenant.ts index d2e2e7cfc7..b144281351 100644 --- a/src/types/Tenant.ts +++ b/src/types/Tenant.ts @@ -9,9 +9,10 @@ export default interface Tenant extends CreatedUpdatedProps { address: Address; logo: string; components: TenantComponent; + cpmsDomainName?: string; // Optional - domain name used to connect chargers redirectDomain?: string; idleMode?: boolean // Prevents batch and async tasks executions when moving the tenant to a different cloud infrastructure provider - taskExecutionEnv?: string; // Environement on which tasks should be executed + taskExecutionEnv?: string; // Environment on which tasks should be executed } export interface TenantComponent { From ca3b761f80eda8ae64585473d4ab46f294e7b384 Mon Sep 17 00:00:00 2001 From: ClaudeROSSI Date: Sun, 26 Mar 2023 11:10:45 +0200 Subject: [PATCH 06/12] tenant - cpms domain name --- .../rest/v1/service/AuthorizationService.ts | 6 +++--- src/utils/Utils.ts | 20 +++++++++++++------ 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/src/server/rest/v1/service/AuthorizationService.ts b/src/server/rest/v1/service/AuthorizationService.ts index 5bd652ce66..662355b388 100644 --- a/src/server/rest/v1/service/AuthorizationService.ts +++ b/src/server/rest/v1/service/AuthorizationService.ts @@ -482,9 +482,9 @@ export default class AuthorizationService { // Optimize data over the net Utils.removeCanPropertiesWithFalseValue(registrationToken); // Build OCPP URLs - registrationToken.ocpp15SOAPSecureUrl = Utils.buildOCPPServerSecureURL(tenant.id, OCPPVersion.VERSION_15, OCPPProtocol.SOAP, registrationToken.id); - registrationToken.ocpp16SOAPSecureUrl = Utils.buildOCPPServerSecureURL(tenant.id, OCPPVersion.VERSION_16, OCPPProtocol.SOAP, registrationToken.id); - registrationToken.ocpp16JSONSecureUrl = Utils.buildOCPPServerSecureURL(tenant.id, OCPPVersion.VERSION_16, OCPPProtocol.JSON, registrationToken.id); + registrationToken.ocpp15SOAPSecureUrl = Utils.buildOCPPServerSecureURL(tenant, OCPPVersion.VERSION_15, OCPPProtocol.SOAP, registrationToken.id); + registrationToken.ocpp16SOAPSecureUrl = Utils.buildOCPPServerSecureURL(tenant, OCPPVersion.VERSION_16, OCPPProtocol.SOAP, registrationToken.id); + registrationToken.ocpp16JSONSecureUrl = Utils.buildOCPPServerSecureURL(tenant, OCPPVersion.VERSION_16, OCPPProtocol.JSON, registrationToken.id); } public static async checkAndGetChargingStationTemplateAuthorizations(tenant: Tenant, userToken: UserToken, diff --git a/src/utils/Utils.ts b/src/utils/Utils.ts index b76aefa553..28e453b1f2 100644 --- a/src/utils/Utils.ts +++ b/src/utils/Utils.ts @@ -927,13 +927,21 @@ export default class Utils { return `${centralSystemFrontEndConfig.protocol}://${centralSystemFrontEndConfig.host}:${centralSystemFrontEndConfig.port}`; } - public static buildOCPPServerSecureURL(tenantID: string, ocppVersion: OCPPVersion, ocppProtocol: OCPPProtocol, token?: string): string { - switch (ocppProtocol) { - case OCPPProtocol.JSON: - return `${Configuration.getJsonEndpointConfig().baseSecureUrl}/${Utils.getOCPPServerVersionURLPath(ocppVersion)}/${tenantID}/${token}`; - case OCPPProtocol.SOAP: - return `${Configuration.getWSDLEndpointConfig().baseSecureUrl}/${Utils.getOCPPServerVersionURLPath(ocppVersion)}?TenantID=${tenantID}%26Token=${token}`; + public static buildOCPPServerSecureURL(tenant: Tenant, ocppVersion: OCPPVersion, ocppProtocol: OCPPProtocol, token?: string): string { + if (ocppProtocol === OCPPProtocol.SOAP) { + const baseSecureUrl = Utils.alterBaseURL(tenant, Configuration.getWSDLEndpointConfig().baseSecureUrl); + return `${baseSecureUrl}/${Utils.getOCPPServerVersionURLPath(ocppVersion)}?TenantID=${tenant.id}%26Token=${token}`; } + const baseSecureUrl = Utils.alterBaseURL(tenant, Configuration.getJsonEndpointConfig().baseSecureUrl); + return `${baseSecureUrl}/${Utils.getOCPPServerVersionURLPath(ocppVersion)}/${tenant.id}/${token}`; + } + + public static alterBaseURL(tenant: Tenant, baseUrl: string) : string { + if (tenant.cpmsDomainName) { + const protocol = baseUrl.split(':').shift(); + baseUrl = `${protocol}://${tenant.cpmsDomainName}`; + } + return baseUrl; } public static getOCPPServerVersionURLPath(ocppVersion: OCPPVersion): string { From 6f73ef8de7119c00b3ae21f027e07431bf1f5b8d Mon Sep 17 00:00:00 2001 From: ClaudeROSSI Date: Mon, 27 Mar 2023 08:33:24 +0200 Subject: [PATCH 07/12] config - cosmetic change --- src/utils/Configuration.ts | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/src/utils/Configuration.ts b/src/utils/Configuration.ts index 7f7bfc07b3..50927188dc 100644 --- a/src/utils/Configuration.ts +++ b/src/utils/Configuration.ts @@ -295,21 +295,15 @@ export default class Configuration { private static getConfig(): ConfigurationData { if (!Configuration.config) { - let configuration: ConfigurationData; + let configurationPath: string; if (process.env.SERVER_ROLE) { - configuration = JSON.parse( - fs.readFileSync(`${global.appRoot}/assets/config_` + process.env.SERVER_ROLE + '.json', 'utf8')) as ConfigurationData; + configurationPath = `${global.appRoot}/assets/config_` + process.env.SERVER_ROLE + '.json'; // Dev environment only + } else if (fs.existsSync('/config/config.json')) { + configurationPath = '/config/config.json'; // K8s Environment } else { - // K8s - if (fs.existsSync('/config/config.json')) { - configuration = JSON.parse( - fs.readFileSync('/config/config.json', 'utf8')) as ConfigurationData; - // AWS - } else { - configuration = JSON.parse( - fs.readFileSync(`${global.appRoot}/assets/config.json`, 'utf8')) as ConfigurationData; - } + configurationPath = `${global.appRoot}/assets/config.json`; // AWS ECS environment only } + const configuration = JSON.parse(fs.readFileSync(configurationPath, 'utf8')) as ConfigurationData; Configuration.config = ConfigurationValidatorStorage.getInstance().validateConfigurationSave(configuration); } return Configuration.config; From e098128fb613d21c2caacc9d4144beecb1820065 Mon Sep 17 00:00:00 2001 From: ClaudeROSSI Date: Mon, 27 Mar 2023 18:15:29 +0200 Subject: [PATCH 08/12] remote push notifications - less logs --- .../RemotePushNotificationTask.ts | 62 ++++++++++--------- 1 file changed, 33 insertions(+), 29 deletions(-) diff --git a/src/notification/remote-push-notification/RemotePushNotificationTask.ts b/src/notification/remote-push-notification/RemotePushNotificationTask.ts index cfffbc274b..66227f3280 100644 --- a/src/notification/remote-push-notification/RemotePushNotificationTask.ts +++ b/src/notification/remote-push-notification/RemotePushNotificationTask.ts @@ -491,20 +491,24 @@ export default class RemotePushNotificationTask implements NotificationTask { // Do it startTime = Logging.traceNotificationStart(); if (!user?.mobileData?.mobileToken) { - await Logging.logDebug({ - tenantID: tenant.id, - siteID: data?.siteID, - siteAreaID: data?.siteAreaID, - companyID: data?.companyID, - chargingStationID: data?.chargeBoxID, - action: ServerAction.REMOTE_PUSH_NOTIFICATION, - module: MODULE_NAME, method: 'sendRemotePushNotificationToUsers', - message: `'${notificationType}': No mobile token found for this User`, - actionOnUser: user.id, - }); + // await Logging.logDebug({ + // tenantID: tenant.id, + // siteID: data?.siteID, + // siteAreaID: data?.siteAreaID, + // companyID: data?.companyID, + // chargingStationID: data?.chargeBoxID, + // action: ServerAction.REMOTE_PUSH_NOTIFICATION, + // module: MODULE_NAME, method: 'sendRemotePushNotificationToUsers', + // message: `'${notificationType}': No mobile token found for this User`, + // actionOnUser: user.id, + // }); // Send nothing return Promise.resolve(); } + if (!user?.mobileData?.mobileVersion) { + // Stop sending notifications to old version of the mobile app + return Promise.resolve(); + } // Create message message = this.createMessage(tenant, notificationType, title, body, data, severity); // Get the right firebase apps @@ -524,7 +528,7 @@ export default class RemotePushNotificationTask implements NotificationTask { ); // Error if (response.failureCount > 0) { - void Logging.logError({ + Logging.logError({ tenantID: tenant.id, siteID: data?.siteID, siteAreaID: data?.siteAreaID, @@ -532,28 +536,28 @@ export default class RemotePushNotificationTask implements NotificationTask { chargingStationID: data?.chargeBoxID, action: ServerAction.REMOTE_PUSH_NOTIFICATION, module: MODULE_NAME, method: 'sendRemotePushNotificationToUsers', - message: `Error when sending Notification: '${notificationType}' - Error code: '${response.results[0]?.error?.code}'`, + message: `Notification: '${notificationType}' - Error code: '${response.results[0]?.error?.code}'`, actionOnUser: user.id, - detailedMessages: { response } - }); + detailedMessages: { response, mobileData: user.mobileData } + }).catch((error) => Logging.logPromiseError(error, tenant.id)); // Success } else { // Stop sending notification notificationSent = true; - void Logging.logDebug({ - tenantID: tenant.id, - siteID: data?.siteID, - siteAreaID: data?.siteAreaID, - companyID: data?.companyID, - chargingStationID: data?.chargeBoxID, - action: ServerAction.REMOTE_PUSH_NOTIFICATION, - module: MODULE_NAME, method: 'sendRemotePushNotificationToUsers', - message: `Notification Sent: '${notificationType}' - '${title}'`, - actionOnUser: user.id, - }); + // Logging.logDebug({ + // tenantID: tenant.id, + // siteID: data?.siteID, + // siteAreaID: data?.siteAreaID, + // companyID: data?.companyID, + // chargingStationID: data?.chargeBoxID, + // action: ServerAction.REMOTE_PUSH_NOTIFICATION, + // module: MODULE_NAME, method: 'sendRemotePushNotificationToUsers', + // message: `Notification Sent: '${notificationType}' - '${title}'`, + // actionOnUser: user.id, + // }).catch((error) => Logging.logPromiseError(error, tenant.id)); } } catch (error) { - void Logging.logError({ + Logging.logError({ tenantID: tenant.id, siteID: data?.siteID, siteAreaID: data?.siteAreaID, @@ -561,10 +565,10 @@ export default class RemotePushNotificationTask implements NotificationTask { chargingStationID: data?.chargeBoxID, action: ServerAction.REMOTE_PUSH_NOTIFICATION, module: MODULE_NAME, method: 'sendRemotePushNotificationToUsers', - message: `Error when sending Notification: '${notificationType}' - '${error.message as string}'`, + message: `Notification: '${notificationType}' - '${error.message as string}'`, actionOnUser: user.id, detailedMessages: { error: error.stack } - }); + }).catch((error2) => Logging.logPromiseError(error2, tenant.id)); } } } finally { From 0edace135681a74924c3ebacfce8bddab326bd5f Mon Sep 17 00:00:00 2001 From: ClaudeROSSI Date: Tue, 28 Mar 2023 09:50:06 +0200 Subject: [PATCH 09/12] remote push notification - logs --- .../remote-push-notification/RemotePushNotificationTask.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/notification/remote-push-notification/RemotePushNotificationTask.ts b/src/notification/remote-push-notification/RemotePushNotificationTask.ts index 66227f3280..992cce7b16 100644 --- a/src/notification/remote-push-notification/RemotePushNotificationTask.ts +++ b/src/notification/remote-push-notification/RemotePushNotificationTask.ts @@ -539,7 +539,7 @@ export default class RemotePushNotificationTask implements NotificationTask { message: `Notification: '${notificationType}' - Error code: '${response.results[0]?.error?.code}'`, actionOnUser: user.id, detailedMessages: { response, mobileData: user.mobileData } - }).catch((error) => Logging.logPromiseError(error, tenant.id)); + }).catch((error) => Logging.logPromiseError(error)); // Success } else { // Stop sending notification @@ -554,7 +554,7 @@ export default class RemotePushNotificationTask implements NotificationTask { // module: MODULE_NAME, method: 'sendRemotePushNotificationToUsers', // message: `Notification Sent: '${notificationType}' - '${title}'`, // actionOnUser: user.id, - // }).catch((error) => Logging.logPromiseError(error, tenant.id)); + // }).catch((error) => Logging.logPromiseError(error)); } } catch (error) { Logging.logError({ @@ -568,7 +568,7 @@ export default class RemotePushNotificationTask implements NotificationTask { message: `Notification: '${notificationType}' - '${error.message as string}'`, actionOnUser: user.id, detailedMessages: { error: error.stack } - }).catch((error2) => Logging.logPromiseError(error2, tenant.id)); + }).catch((error2) => Logging.logPromiseError(error2)); } } } finally { From 391ad80987b073b40ced2ee63cf52b0dcc98b2ac Mon Sep 17 00:00:00 2001 From: ClaudeROSSI Date: Tue, 28 Mar 2023 10:10:57 +0200 Subject: [PATCH 10/12] UpdateChargingStationWithTemplate - less verbose --- src/server/ocpp/utils/OCPPUtils.ts | 38 +++++++++++++++++++++++------- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/src/server/ocpp/utils/OCPPUtils.ts b/src/server/ocpp/utils/OCPPUtils.ts index a73ca740fe..0c44f48ad1 100644 --- a/src/server/ocpp/utils/OCPPUtils.ts +++ b/src/server/ocpp/utils/OCPPUtils.ts @@ -801,7 +801,10 @@ export default class OCPPUtils { action: ServerAction.UPDATE_CHARGING_STATION_WITH_TEMPLATE, module: MODULE_NAME, method: 'enrichChargingStationConnectorWithTemplate', message: `Template for Connector ID '${connector.connectorId}' cannot be applied on manual configured charging station`, - detailedMessages: { chargingStation, connector } + detailedMessages: { + chargingStationData: LoggingHelper.shrinkChargingStationProperties(chargingStation), + connector + } }); return false; } @@ -880,7 +883,6 @@ export default class OCPPUtils { action: ServerAction.UPDATE_CHARGING_STATION_WITH_TEMPLATE, module: MODULE_NAME, method: 'enrichChargingStationConnectorWithTemplate', message: `Template ID '${chargingStationTemplate.id}' has been applied on Connector ID '${connector.connectorId}' with success`, - detailedMessages: { chargingStationTemplate, chargingStation } }); return true; } @@ -891,7 +893,10 @@ export default class OCPPUtils { action: ServerAction.UPDATE_CHARGING_STATION_WITH_TEMPLATE, module: MODULE_NAME, method: 'enrichChargingStationConnectorWithTemplate', message: `No Connector found in Template ID '${chargingStationTemplate.id}'`, - detailedMessages: { chargingStationTemplate, chargingStation } + detailedMessages: { + templateData: LoggingHelper.shrinkTemplateProperties(chargingStationTemplate), + chargingStationData: LoggingHelper.shrinkChargingStationProperties(chargingStation) + } }); return false; } @@ -902,7 +907,10 @@ export default class OCPPUtils { action: ServerAction.UPDATE_CHARGING_STATION_WITH_TEMPLATE, module: MODULE_NAME, method: 'enrichChargingStationConnectorWithTemplate', message: 'No Template has been found for this Charging Station', - detailedMessages: { chargingStation, connector } + detailedMessages: { + chargingStationData: LoggingHelper.shrinkChargingStationProperties(chargingStation), + connector + } }); return false; } @@ -1521,7 +1529,9 @@ export default class OCPPUtils { action: ServerAction.UPDATE_CHARGING_STATION_WITH_TEMPLATE, module: MODULE_NAME, method: 'enrichChargingStationWithTemplate', message: 'Template cannot be applied on manual configured charging station', - detailedMessages: { chargingStation } + detailedMessages: { + chargingStationData: LoggingHelper.shrinkChargingStationProperties(chargingStation) + } }); return templateUpdateResult; } @@ -1648,7 +1658,9 @@ export default class OCPPUtils { action: ServerAction.UPDATE_CHARGING_STATION_WITH_TEMPLATE, module: MODULE_NAME, method: 'enrichChargingStationWithTemplate', message: `Template contains power limitation key '${parameter}' in OCPP parameters, skipping. Remove it from template!`, - detailedMessages: { chargingStationTemplate } + detailedMessages: { + templateData: LoggingHelper.shrinkTemplateProperties(chargingStationTemplate) + } }); continue; } @@ -1659,7 +1671,9 @@ export default class OCPPUtils { action: ServerAction.UPDATE_CHARGING_STATION_WITH_TEMPLATE, module: MODULE_NAME, method: 'enrichChargingStationWithTemplate', message: `Template contains heartbeat interval key '${parameter}' in OCPP parameters, skipping. Remove it from template`, - detailedMessages: { chargingStationTemplate } + detailedMessages: { + templateData: LoggingHelper.shrinkTemplateProperties(chargingStationTemplate) + } }); continue; } @@ -1678,7 +1692,10 @@ export default class OCPPUtils { action: ServerAction.UPDATE_CHARGING_STATION_WITH_TEMPLATE, module: MODULE_NAME, method: 'enrichChargingStationWithTemplateOcppParams', message: `Cannot find a matching section named '${ocppProperty}' in Template ID '${chargingStationTemplate.id}'`, - detailedMessages: { chargingStationTemplate, chargingStation } + detailedMessages: { + templateData: LoggingHelper.shrinkTemplateProperties(chargingStationTemplate), + chargingStationData: LoggingHelper.shrinkChargingStationProperties(chargingStation) + } }); } } @@ -1725,7 +1742,10 @@ export default class OCPPUtils { action: ServerAction.UPDATE_CHARGING_STATION_WITH_TEMPLATE, module: MODULE_NAME, method: 'enrichChargingStationWithTemplateCapabilities', message: `Cannot find a matching section named 'capabilities' in Template ID '${chargingStationTemplate.id}'`, - detailedMessages: { chargingStationTemplate, chargingStation } + detailedMessages: { + templateData: LoggingHelper.shrinkTemplateProperties(chargingStationTemplate), + chargingStationData: LoggingHelper.shrinkChargingStationProperties(chargingStation) + } }); } } From f39ca5900ae8df9226712588573507edd57cd60c Mon Sep 17 00:00:00 2001 From: ClaudeROSSI Date: Tue, 28 Mar 2023 10:19:59 +0200 Subject: [PATCH 11/12] UpdateChargingStationWithTemplate - less verbose --- src/server/ocpp/utils/OCPPUtils.ts | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/server/ocpp/utils/OCPPUtils.ts b/src/server/ocpp/utils/OCPPUtils.ts index 0c44f48ad1..b237414fcc 100644 --- a/src/server/ocpp/utils/OCPPUtils.ts +++ b/src/server/ocpp/utils/OCPPUtils.ts @@ -1573,17 +1573,6 @@ export default class OCPPUtils { templateData: LoggingHelper.shrinkTemplateProperties(chargingStationTemplate), } }); - } else { - await Logging.logDebug({ - ...LoggingHelper.getChargingStationProperties(chargingStation), - tenantID: tenant.id, - action: ServerAction.UPDATE_CHARGING_STATION_WITH_TEMPLATE, - module: MODULE_NAME, method: 'enrichChargingStationWithTemplate', - message: `Template ID '${chargingStationTemplate.id}' has already been applied`, - detailedMessages: { - templateData: LoggingHelper.shrinkTemplateProperties(chargingStationTemplate) - } - }); } // Master/Slave: always override the charge point if (chargingStationTemplate.template.technical.masterSlave) { From ff4e4fcfe6cd2e5504e7ded9a6a4dd5a0b5e01fd Mon Sep 17 00:00:00 2001 From: Ramzi Siala <1226644+Ramzay@users.noreply.github.com> Date: Tue, 28 Mar 2023 12:58:20 +0200 Subject: [PATCH 12/12] Fix delete pricing on deleted CS --- src/server/rest/v1/service/PricingService.ts | 2 +- src/server/rest/v1/service/UtilsService.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/server/rest/v1/service/PricingService.ts b/src/server/rest/v1/service/PricingService.ts index f4ca032361..b7f04fcd49 100644 --- a/src/server/rest/v1/service/PricingService.ts +++ b/src/server/rest/v1/service/PricingService.ts @@ -229,7 +229,7 @@ export default class PricingService { siteID = siteArea.siteID; break; case PricingEntity.CHARGING_STATION: - chargingStation = await UtilsService.checkAndGetChargingStationAuthorization(req.tenant, req.user, entityID, Action.READ, action); + chargingStation = await UtilsService.checkAndGetChargingStationAuthorization(req.tenant, req.user, entityID, Action.READ, action, null, { includeDeleted: true }); siteID = chargingStation.siteID; break; default: diff --git a/src/server/rest/v1/service/UtilsService.ts b/src/server/rest/v1/service/UtilsService.ts index e8c4780d96..5fc97750f7 100644 --- a/src/server/rest/v1/service/UtilsService.ts +++ b/src/server/rest/v1/service/UtilsService.ts @@ -128,7 +128,7 @@ export default class UtilsService { UtilsService.assertObjectExists(action, chargingStation, `ChargingStation ID '${chargingStationID}' does not exist`, MODULE_NAME, 'checkAndGetChargingStationAuthorization', userToken); // Check deleted - if (chargingStation?.deleted) { + if (!additionalFilters?.includeDeleted && chargingStation?.deleted) { throw new AppError({ ...LoggingHelper.getChargingStationProperties(chargingStation), errorCode: StatusCodes.NOT_FOUND,