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-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": [ 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 3454ec4fb3..5cc1f32f59 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: { @@ -553,6 +559,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: { @@ -1518,6 +1530,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: { @@ -1926,6 +1960,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: { @@ -2632,6 +2688,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: { @@ -3120,6 +3198,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/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); } diff --git a/src/server/rest/v1/service/AuthorizationService.ts b/src/server/rest/v1/service/AuthorizationService.ts index 07d0000cb8..662355b388 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'; @@ -481,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, @@ -1489,6 +1490,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 5d294ec62d..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,23 +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 if (filteredRequest.SiteID) { - filter.siteIDs = filteredRequest.SiteID.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; } @@ -666,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 fbf56e0b39..a05e9892d4 100644 --- a/src/types/DataResult.ts +++ b/src/types/DataResult.ts @@ -191,3 +191,11 @@ export interface OcpiEndpointDataResult extends DataResult { canPing?: boolean; canGenerateLocalToken?: boolean; } + +export interface StatisticDataResult extends DataResult { + canListUsers?: boolean; + canListChargingStations?: boolean; + canListSites?: boolean; + canListSiteAreas?: boolean; + canExport?: boolean; +} 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 { 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 { 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; diff --git a/src/utils/Utils.ts b/src/utils/Utils.ts index f5399f9afd..28e453b1f2 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 { @@ -932,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 {