Admin Update/Create/Assign/Unassign cars/users,
Manage Lists off line
MODIFY
src/authorization/Authorizations.ts
src/authorization/Authorizations.ts
@@ -469,6 +469,22 @@ export default class Authorizations {
return Authorizations.canPerformAction(loggedUser, Entity.CARS, Action.LIST);
}
+ public static canReadCar(loggedUser: UserToken): boolean {
+ return Authorizations.canPerformAction(loggedUser, Entity.CAR, Action.READ);
+ }
+
+ public static canListUsersCars(loggedUser: UserToken): boolean {
+ return Authorizations.canPerformAction(loggedUser, Entity.USERS_CARS, Action.LIST);
+ }
+
+ public static canAssignUsersCar(loggedUser: UserToken): boolean {
+ return Authorizations.canPerformAction(loggedUser, Entity.USER_CAR, Action.CREATE);
+ }
+
+ public static canUpdateUsersCar(loggedUser: UserToken): boolean {
+ return Authorizations.canPerformAction(loggedUser, Entity.USERS_CARS, Action.UPDATE);
+ }
+
public static canSynchronizeCarCatalogs(loggedUser: UserToken): boolean {
return Authorizations.canPerformAction(loggedUser, Entity.CAR_CATALOGS, Action.SYNCHRONIZE_CAR_CATALOGS);
}
@@ -477,6 +493,10 @@ export default class Authorizations {
return Authorizations.canPerformAction(loggedUser, Entity.CAR, Action.CREATE);
}
+ public static canUpdateCar(loggedUser: UserToken): boolean {
+ return Authorizations.canPerformAction(loggedUser, Entity.CAR, Action.UPDATE);
+ }
+
public static canListAssets(loggedUser: UserToken): boolean {
return Authorizations.canPerformAction(loggedUser, Entity.ASSETS, Action.LIST);
}

MODIFY
src/authorization/AuthorizationsDefinition.ts
src/authorization/AuthorizationsDefinition.ts
@@ -82,6 +82,11 @@ const AUTHORIZATION_DEFINITION: AuthorizationDefinition = {
{ resource: Entity.CAR_CATALOG, action: Action.READ, attributes: ['*'] },
{ resource: Entity.CAR, action: Action.CREATE, attributes: ['*'] },
{ resource: Entity.CARS, action: Action.LIST, attributes: ['*'] },
+ { resource: Entity.USERS_CARS, action: Action.LIST, attributes: ['*'] },
+ { resource: Entity.USER_CAR, action: Action.CREATE, attributes: ['*'] },
+ { resource: Entity.CAR, action: Action.READ, attributes: ['*'] },
+ { resource: Entity.CAR, action: Action.UPDATE, attributes: ['*'] },
+ { resource: Entity.USERS_CARS, action: Action.UPDATE, attributes: ['*'] },
]
},
basic: {
@@ -97,6 +102,8 @@ const AUTHORIZATION_DEFINITION: AuthorizationDefinition = {
{ resource: Entity.CAR_CATALOG, action: Action.READ, attributes: ['*'] },
{ resource: Entity.CAR, action: Action.CREATE, attributes: ['*'] },
{ resource: Entity.CARS, action: Action.LIST, attributes: ['*'] },
+ { resource: Entity.CAR, action: Action.UPDATE, attributes: ['*'] },
+ { resource: Entity.USERS_CARS, action: Action.UPDATE, attributes: ['*'] },
{
resource: Entity.COMPANY, action: Action.READ, attributes: ['*'],
condition: { Fn: 'LIST_CONTAINS', args: { 'companies': '$.company' } }

MODIFY
src/server/rest/CentralRestServerService.ts
src/server/rest/CentralRestServerService.ts
@@ -95,7 +95,8 @@ class RequestMapper {
[ServerAction.OCPI_ENPOINT_GENERATE_LOCAL_TOKEN]: OCPIEndpointService.handleGenerateLocalTokenOcpiEndpoint.bind(this),
[ServerAction.INTEGRATION_CONNECTION_CREATE]: ConnectionService.handleCreateConnection.bind(this),
[ServerAction.CHARGING_STATION_REQUEST_OCPP_PARAMETERS]: ChargingStationService.handleRequestChargingStationOcppParameters.bind(this),
- [ServerAction.CAR_CREATE]: CarService.handleCarCreate.bind(this)
+ [ServerAction.CAR_CREATE]: CarService.handleCarCreate.bind(this),
+ [ServerAction.ASSIGN_USERS_CAR]: CarService.handleAssignUsersCar.bind(this)
});
break;
@@ -111,6 +112,8 @@ class RequestMapper {
[ServerAction.CAR_CATALOG]: CarService.handleGetCarCatalog.bind(this),
[ServerAction.CAR_MAKERS]: CarService.handleGetCarMakers.bind(this),
[ServerAction.CARS]: CarService.handleGetCars.bind(this),
+ [ServerAction.CAR]: CarService.handleGetCar.bind(this),
+ [ServerAction.USERS_CAR]: CarService.handleGetUsersCar.bind(this),
[ServerAction.CAR_CATALOG_IMAGES]: CarService.handleGetCarCatalogImages.bind(this),
[ServerAction.CHARGING_STATIONS_EXPORT]: ChargingStationService.handleGetChargingStationsExport.bind(this),
[ServerAction.CHARGING_STATIONS_OCPP_PARAMS_EXPORT]: ChargingStationService.handleChargingStationsOCPPParamsExport.bind(this),
@@ -206,6 +209,8 @@ class RequestMapper {
[ServerAction.OCPI_ENDPOINT_REGISTER]: OCPIEndpointService.handleRegisterOcpiEndpoint.bind(this),
[ServerAction.OCPI_ENDPOINT_UNREGISTER]: OCPIEndpointService.handleUnregisterOcpiEndpoint.bind(this),
[ServerAction.SYNCHRONIZE_CAR_CATALOGS]: CarService.handleSynchronizeCarCatalogs.bind(this),
+ [ServerAction.CAR_UPDATE]: CarService.handleUpdateCar.bind(this),
+ [ServerAction.USER_CAR_UPDATE]: CarService.handleUpdateUsersCar.bind(this),
});
break;

MODIFY
src/server/rest/service/CarService.ts
src/server/rest/service/CarService.ts
@@ -3,6 +3,7 @@ import { Car, UserCar } from '../../../types/Car';
import { HTTPAuthError, HTTPError } from '../../../types/HTTPError';
import { NextFunction, Request, Response } from 'express';
+import { ActionsResponse } from '../../../types/GlobalType';
import AppAuthError from '../../../exception/AppAuthError';
import AppError from '../../../exception/AppError';
import Authorizations from '../../../authorization/Authorizations';
@@ -251,6 +252,7 @@ export default class CarService {
licensePlate: filteredRequest.licensePlate,
vin: filteredRequest.vin,
createdBy: { id: req.user.id },
+ type: filteredRequest.type,
createdOn: new Date()
} as Car;
newCar.id = await CarStorage.saveCar(req.user.tenantID, newCar);
@@ -268,7 +270,6 @@ export default class CarService {
const newUserCar: UserCar = {
carID: newCar.id,
userID: req.user.id,
- type: filteredRequest.type,
default: !defaultCar,
createdBy: { id: req.user.id },
createdOn: new Date()
@@ -283,6 +284,79 @@ export default class CarService {
next();
}
+ public static async handleUpdateCar(action: ServerAction, req: Request, res: Response, next: NextFunction): Promise<void> {
+ // Check if component is active
+ UtilsService.assertComponentIsActiveFromToken(req.user, TenantComponents.ORGANIZATION,
+ Action.UPDATE, Entity.SITE, MODULE_NAME, 'handleUpdateSite');
+ // Filter
+ const filteredRequest = CarSecurity.filterCarUpdateRequest(req.body);
+ // Check
+ Utils.checkIfCarValid(filteredRequest, req);
+ // Check auth
+ if (!Authorizations.canUpdateCar(req.user)) {
+ throw new AppAuthError({
+ errorCode: HTTPAuthError.ERROR,
+ user: req.user,
+ action: Action.UPDATE,
+ entity: Entity.CAR,
+ module: MODULE_NAME,
+ method: 'handleUpdateCar',
+ value: filteredRequest.id
+ });
+ }
+ // Check Car
+ const car = await CarStorage.getCar(req.user.tenantID, filteredRequest.id);
+ UtilsService.assertObjectExists(action, car, `Car ID '${filteredRequest.id}' does not exist`,
+ MODULE_NAME, 'handleUpdateCar', req.user);
+ if (car.licensePlate !== filteredRequest.licensePlate || car.vin !== filteredRequest.vin) {
+ const checkCar = await CarStorage.getCarByVinLicensePlate(req.user.tenantID, filteredRequest.licensePlate, filteredRequest.vin);
+ if (checkCar) {
+ throw new AppError({
+ source: Constants.CENTRAL_SERVER,
+ errorCode: HTTPError.CAR_ALREADY_EXIST_ERROR,
+ message: `The Car with VIN: '${filteredRequest.vin}' and License plate: '${filteredRequest.licensePlate}' already exist`,
+ user: req.user,
+ module: MODULE_NAME, method: 'handleUpdateCar'
+ });
+ }
+ }
+ car.carCatalogID = filteredRequest.carCatalogID;
+ car.vin = filteredRequest.vin;
+ car.licensePlate = filteredRequest.licensePlate;
+ car.type = filteredRequest.type;
+ car.lastChangedBy = { 'id': req.user.id };
+ car.lastChangedOn = new Date();
+ await CarStorage.saveCar(req.user.tenantID, car);
+ // Log
+ Logging.logSecurityInfo({
+ tenantID: req.user.tenantID,
+ user: req.user, module: MODULE_NAME, method: 'handleUpdateCar',
+ message: `Car '${car.id}' has been updated successfully`,
+ action: action,
+ detailedMessages: { car }
+ });
+ if (Authorizations.isBasic(req.user)) {
+ const userCar = await CarStorage.getUserCarByCarUser(req.user.tenantID, car.id, req.user.id);
+ if (userCar.default !== filteredRequest.isDefault) {
+ userCar.default = filteredRequest.isDefault;
+ userCar.lastChangedBy = { 'id': req.user.id };
+ userCar.lastChangedOn = new Date();
+ await CarStorage.saveUserCar(req.user.tenantID, userCar);
+ // Log
+ Logging.logSecurityInfo({
+ tenantID: req.user.tenantID,
+ user: req.user, module: MODULE_NAME, method: 'handleUpdateCar',
+ message: `User Car '${userCar.id}' has been updated successfully`,
+ action: action,
+ detailedMessages: { userCar }
+ });
+ }
+ }
+ // Ok
+ res.json(Constants.REST_RESPONSE_SUCCESS);
+ next();
+ }
+
public static async handleGetCars(action: Action, req: Request, res: Response, next: NextFunction): Promise<void> {
// Check if component is active
UtilsService.assertComponentIsActiveFromToken(req.user, TenantComponents.CAR, Action.LIST, Entity.CARS,
@@ -310,4 +384,97 @@ export default class CarService {
res.json(cars);
next();
}
+
+ public static async handleGetCar(action: ServerAction, req: Request, res: Response, next: NextFunction): Promise<void> {
+ // Check if component is active
+ UtilsService.assertComponentIsActiveFromToken(req.user, TenantComponents.CAR, Action.READ, Entity.CAR,
+ MODULE_NAME, 'handleGetCar');
+ // Check auth
+ if (!Authorizations.canReadCar(req.user)) {
+ throw new AppAuthError({
+ errorCode: HTTPAuthError.ERROR,
+ user: req.user,
+ action: Action.READ, entity: Entity.CAR,
+ module: MODULE_NAME, method: 'handleGetCar'
+ });
+ }
+ const filteredRequest = CarSecurity.filterCarRequest(req.query);
+ UtilsService.assertIdIsProvided(action, filteredRequest.ID, MODULE_NAME, 'handleGetCarCatalog', req.user);
+
+ // Get the car
+ const car = await CarStorage.getCar(req.user.tenantID, filteredRequest.ID);
+
+ // Return
+ res.json(CarSecurity.filterCarResponse(car, req.user));
+ next();
+ }
+
+ public static async handleGetUsersCar(action: Action, req: Request, res: Response, next: NextFunction): Promise<void> {
+ // Check if component is active
+ UtilsService.assertComponentIsActiveFromToken(req.user, TenantComponents.CAR, Action.LIST, Entity.USERS_CARS,
+ MODULE_NAME, 'handleGetUsersCar');
+ // Check auth
+ if (!Authorizations.canListUsersCars(req.user)) {
+ throw new AppAuthError({
+ errorCode: HTTPAuthError.ERROR,
+ user: req.user,
+ action: Action.LIST, entity: Entity.USERS_CARS,
+ module: MODULE_NAME, method: 'handleGetUsersCar'
+ });
+ }
+ const filteredRequest = CarSecurity.filterUsersCarsRequest(req.query);
+ // Get cars
+ const usersCars = await CarStorage.getUsersCars(req.user.tenantID,
+ {
+ search: filteredRequest.search,
+ carIDs: [filteredRequest.carID],
+ active: true
+ },
+ { limit: filteredRequest.Limit, skip: filteredRequest.Skip, sort: filteredRequest.Sort, onlyRecordCount: filteredRequest.OnlyRecordCount });
+ // Filter
+ CarSecurity.filterUsersCarsResponse(usersCars, req.user);
+ // Return
+ res.json(usersCars);
+ next();
+ }
+
+ public static async handleAssignUsersCar(action: Action, req: Request, res: Response, next: NextFunction): Promise<void> {
+ // Check if component is active
+ UtilsService.assertComponentIsActiveFromToken(req.user, TenantComponents.CAR, Action.CREATE, Entity.USER_CAR,
+ MODULE_NAME, 'handleAssignUsersCar');
+ // Check auth
+ if (!Authorizations.canAssignUsersCar(req.user)) {
+ throw new AppAuthError({
+ errorCode: HTTPAuthError.ERROR,
+ user: req.user,
+ action: Action.LIST, entity: Entity.USERS_CARS,
+ module: MODULE_NAME, method: 'handleAssignUsersCar'
+ });
+ }
+ const filteredRequest = CarSecurity.filterUsersAssignRequest(req.body);
+ const result = await CarStorage.assignUsersCar(req.user.tenantID, filteredRequest.carID, filteredRequest.usersCar);
+ // Return
+ res.json({ ...result, ...Constants.REST_RESPONSE_SUCCESS });
+ next();
+ }
+
+ public static async handleUpdateUsersCar(action: Action, req: Request, res: Response, next: NextFunction): Promise<void> {
+ // Check if component is active
+ UtilsService.assertComponentIsActiveFromToken(req.user, TenantComponents.CAR, Action.UPDATE, Entity.USER_CAR,
+ MODULE_NAME, 'handleUpdateUsersCar');
+ // Check auth
+ if (!Authorizations.canUpdateUsersCar(req.user)) {
+ throw new AppAuthError({
+ errorCode: HTTPAuthError.ERROR,
+ user: req.user,
+ action: Action.UPDATE, entity: Entity.USERS_CARS,
+ module: MODULE_NAME, method: 'handleUpdateUsersCar'
+ });
+ }
+ const filteredRequest = CarSecurity.filterUsersAssignRequest(req.body);
+ const result = await CarStorage.updateUsersCar(req.user.tenantID, filteredRequest.carID, filteredRequest.usersCar);
+
+ res.json({ ...result, ...Constants.REST_RESPONSE_SUCCESS });
+ next();
+ }
}

MODIFY
src/server/rest/service/UserService.ts
src/server/rest/service/UserService.ts
@@ -815,6 +815,10 @@ export default class UserService {
UtilsService.assertComponentIsActiveFromToken(req.user, TenantComponents.ORGANIZATION,
Action.READ, Entity.USER, MODULE_NAME, 'handleGetUsers');
}
+ if (filteredRequest.ExcludeUserIDs && filteredRequest.ExcludeUserIDs.length > 0) {
+ UtilsService.assertComponentIsActiveFromToken(req.user, TenantComponents.CAR,
+ Action.READ, Entity.USER, MODULE_NAME, 'handleGetUsers');
+ }
// Get users
const users = await UserStorage.getUsers(req.user.tenantID,
{
@@ -824,6 +828,7 @@ export default class UserService {
roles: (filteredRequest.Role ? filteredRequest.Role.split('|') : null),
statuses: (filteredRequest.Status ? filteredRequest.Status.split('|') : null),
excludeSiteID: filteredRequest.ExcludeSiteID,
+ excludeUserIDs: (filteredRequest.ExcludeUserIDs ? filteredRequest.ExcludeUserIDs.split('|') : null)
},
{
limit: filteredRequest.Limit,

MODIFY
src/server/rest/service/security/CarSecurity.ts
src/server/rest/service/security/CarSecurity.ts
-import { Car, CarCatalog, CarMaker } from '../../../../types/Car';
-import { HttpCarCatalogByIDRequest, HttpCarCatalogImagesRequest, HttpCarCatalogsRequest, HttpCarCreateRequest, HttpCarMakersRequest, HttpCarsRequest } from '../../../../types/requests/HttpCarRequest';
+import { Car, CarCatalog, CarMaker, UserCar } from '../../../../types/Car';
+import { HttpCarByIDRequest, HttpCarCatalogByIDRequest, HttpCarCatalogImagesRequest, HttpCarCatalogsRequest, HttpCarCreateRequest, HttpCarMakersRequest, HttpCarsRequest, HttpUsersAssignRequest, HttpUsersCarsRequest } from '../../../../types/requests/HttpCarRequest';
import Authorizations from '../../../../authorization/Authorizations';
import { DataResult } from '../../../../types/DataResult';
@@ -160,6 +160,17 @@ export default class CarSecurity {
};
}
+ public static filterCarUpdateRequest(request: any): HttpCarCreateRequest {
+ return {
+ vin: sanitize(request.vin),
+ licensePlate: sanitize(request.licensePlate),
+ carCatalogID: Utils.convertToInt(sanitize(request.carCatalogID)),
+ type: sanitize(request.type),
+ isDefault: UtilsSecurity.filterBoolean(request.isDefault),
+ id: sanitize(request.id)
+ };
+ }
+
public static filterCarsResponse(cars: DataResult<Car>, loggedUser: UserToken) {
const filteredCars: Car[] = [];
if (!cars.result) {
@@ -207,6 +218,39 @@ export default class CarSecurity {
return filteredCar;
}
+ public static filterUsersCarsResponse(usersCars: DataResult<UserCar>, loggedUser: UserToken) {
+ const filteredUsersCars: UserCar[] = [];
+ if (!usersCars.result) {
+ return null;
+ }
+ if (!Authorizations.canListUsersCars(loggedUser)) {
+ return null;
+ }
+ for (const userCar of usersCars.result) {
+ // Add
+ if (userCar) {
+ filteredUsersCars.push(
+ this.filterUserCarResponse(userCar, loggedUser)
+ );
+ }
+ }
+ usersCars.result = filteredUsersCars;
+ }
+
+ public static filterUserCarResponse(userCar: UserCar, loggedUser: UserToken): UserCar {
+ if (!userCar) {
+ return null;
+ }
+ const filteredUserCar: UserCar = {
+ id: userCar.id,
+ user: userCar.user,
+ carID: userCar.carID,
+ userID: userCar.userID,
+ default: userCar.default
+ };
+ return filteredUserCar;
+ }
+
public static filterCarsRequest(request: any): HttpCarsRequest {
const filteredRequest: HttpCarsRequest = {
Search: sanitize(request.Search),
@@ -216,4 +260,37 @@ export default class CarSecurity {
UtilsSecurity.filterSort(request, filteredRequest);
return filteredRequest;
}
+
+ public static filterCarRequest(request: any): HttpCarByIDRequest {
+ const filteredRequest: HttpCarByIDRequest = {
+ ID: sanitize(request.CarID),
+ } as HttpCarByIDRequest;
+ return filteredRequest;
+ }
+
+ public static filterUsersCarsRequest(request: any): HttpUsersCarsRequest {
+ const filteredRequest: HttpUsersCarsRequest = {
+ search: sanitize(request.search),
+ carID: sanitize(request.carID),
+ } as HttpUsersCarsRequest;
+ UtilsSecurity.filterSkipAndLimit(request, filteredRequest);
+ UtilsSecurity.filterSort(request, filteredRequest);
+ return filteredRequest;
+ }
+
+ public static filterUsersAssignRequest(request: any): HttpUsersAssignRequest {
+ const usersCar: UserCar[] = [];
+ if (request.usersCar && request.usersCar.length > 0) {
+ for (const userCar of request.usersCar) {
+ usersCar.push({
+ userID: userCar.user.id,
+ default: userCar.default,
+ } as UserCar);
+ }
+ }
+ return {
+ carID: request.carID,
+ usersCar: usersCar
+ };
+ }
}

MODIFY
src/server/rest/service/security/UserSecurity.ts
src/server/rest/service/security/UserSecurity.ts
@@ -46,6 +46,9 @@ export default class UserSecurity {
if (request.ExcludeSiteID) {
request.ExcludeSiteID = sanitize(request.ExcludeSiteID);
}
+ if (request.ExcludeUserIDs) {
+ request.ExcludeUserIDs = sanitize(request.ExcludeUserIDs);
+ }
UtilsSecurity.filterSkipAndLimit(request, request);
UtilsSecurity.filterSort(request, request);
return request as HttpUsersRequest;

MODIFY
src/storage/mongodb/CarStorage.ts
src/storage/mongodb/CarStorage.ts
-import { Car, CarCatalog, CarMaker, ChargeAlternativeTable, ChargeOptionTable, ChargeStandardTable, UserCar } from '../../types/Car';
-import global, { Image } from '../../types/GlobalType';
+import { Car, CarCatalog, CarMaker, CarType, ChargeAlternativeTable, ChargeOptionTable, ChargeStandardTable, UserCar } from '../../types/Car';
+import global, { ActionsResponse, Image } from '../../types/GlobalType';
import Constants from '../../utils/Constants';
import Cypher from '../../utils/Cypher';
@@ -8,8 +8,8 @@ import DatabaseUtils from './DatabaseUtils';
import DbParams from '../../types/database/DbParams';
import Logging from '../../utils/Logging';
import { ObjectID } from 'mongodb';
+import { ServerAction } from '../../types/Server';
import Utils from '../../utils/Utils';
-import { type } from 'os';
const MODULE_NAME = 'CarStorage';
@@ -464,6 +464,7 @@ export default class CarStorage {
vin: carToSave.vin,
licensePlate: carToSave.licensePlate,
carCatalogID: Utils.convertToInt(carToSave.carCatalogID),
+ type: carToSave.type
};
// Add Last Changed/Created props
DatabaseUtils.addLastChangedCreatedProps(carMDB, carToSave);
@@ -488,14 +489,17 @@ export default class CarStorage {
_id: userCarToSave.id ? Utils.convertToObjectID(userCarToSave.id) : new ObjectID(),
userID: Utils.convertToObjectID(userCarToSave.userID),
carID: Utils.convertToObjectID(userCarToSave.carID),
- type: userCarToSave.type,
- default: userCarToSave.default
+ default: userCarToSave.default,
+ active: true
};
// Add Last Changed/Created props
DatabaseUtils.addLastChangedCreatedProps(userCarMDB, userCarToSave);
// Modify
await global.database.getCollection(tenantID, 'userscars').findOneAndUpdate(
- { _id: userCarMDB._id },
+ {
+ _id: userCarMDB._id
+ },
+
{ $set: userCarMDB },
{ upsert: true, returnOriginal: false }
);
@@ -504,6 +508,102 @@ export default class CarStorage {
return userCarMDB._id.toHexString();
}
+ public static async assignUsersCar(tenantID: string, carID: string, usersCarToSave: UserCar[]): Promise<ActionsResponse> {
+ // Debug
+ const uniqueTimerID = Logging.traceStart(MODULE_NAME, 'AssignUsersCar');
+ const actionsDone: ActionsResponse = {
+ inSuccess: 0,
+ inError: 0
+ };
+ // Check Tenant
+ await Utils.checkTenant(tenantID);
+ // Set
+ for (const userCar of usersCarToSave) {
+ try {
+ const isDefault = userCar.default;
+ const defaultCar = await CarStorage.getDefaultCar(tenantID, userCar.userID);
+ userCar.carID = carID;
+ userCar.default = !defaultCar;
+ userCar.createdOn = new Date();
+ userCar.active = true;
+ await CarStorage.saveUserCar(tenantID, userCar);
+ if (defaultCar && isDefault) {
+ await CarStorage.updateDefaultCar(tenantID, carID, userCar.userID);
+ }
+ actionsDone.inSuccess++;
+ } catch (error) {
+ actionsDone.inError++;
+ }
+ }
+ // Log
+ Utils.logActionsResponse(tenantID, ServerAction.ASSIGN_USERS_CAR,
+ MODULE_NAME, 'AssignUsersCar', actionsDone,
+ '{{inSuccess}} user(s) were successfully assigned',
+ '{{inError}} user(s) failed to be assigned',
+ '{{inSuccess}} user(s) were successfully assigned and {{inError}} failed to be assigned',
+ 'All the users are up to date'
+ );
+ return actionsDone;
+ }
+
+ public static async updateUsersCar(tenantID: string, carID: string, usersCarToSave: UserCar[]): Promise<ActionsResponse> {
+ // Debug
+ const uniqueTimerID = Logging.traceStart(MODULE_NAME, 'updateUsersCar');
+ const actionsDone: ActionsResponse = {
+ inSuccess: 0,
+ inError: 0
+ };
+ const deactivated = await global.database.getCollection(tenantID, 'userscars').updateMany({
+ $and: [{ carID: Utils.convertToObjectID(carID) },
+ { userID: { $nin: usersCarToSave.map((userCarToSave) => Utils.convertToObjectID(userCarToSave.userID)) } }]
+ }, {
+ $set: {
+ active: false
+ }
+ }
+ );
+ actionsDone.inSuccess += deactivated.modifiedCount;
+ // Check Tenant
+ await Utils.checkTenant(tenantID);
+ // Set
+ for (const userCar of usersCarToSave) {
+ try {
+ const userCarDB = await CarStorage.getUserCarByCarUser(tenantID, carID, userCar.userID);
+ if (userCarDB) {
+ if (userCarDB.default !== userCar.default || !userCar.active) {
+ userCarDB.active = true;
+ userCarDB.default = userCar.default;
+ await CarStorage.saveUserCar(tenantID, userCarDB);
+ actionsDone.inSuccess++;
+ }
+ } else {
+ const isDefault = userCar.default;
+ const defaultCar = await CarStorage.getDefaultCar(tenantID, userCar.userID);
+ userCar.carID = carID;
+ userCar.default = !defaultCar;
+ userCar.lastChangedOn = new Date();
+ userCar.active = true;
+ await CarStorage.saveUserCar(tenantID, userCar);
+ if (defaultCar && isDefault) {
+ await CarStorage.updateDefaultCar(tenantID, carID, userCar.userID);
+ }
+ actionsDone.inSuccess++;
+ }
+ } catch (error) {
+ actionsDone.inError++;
+ }
+ }
+ // Log
+ Utils.logActionsResponse(tenantID, ServerAction.ASSIGN_USERS_CAR,
+ MODULE_NAME, 'updateUsersCar', actionsDone,
+ '{{inSuccess}} user(s) were successfully updated',
+ '{{inError}} user(s) failed to be updated',
+ '{{inSuccess}} user(s) were successfully updated and {{inError}} failed to be updated',
+ 'All the users are up to date'
+ );
+ return actionsDone;
+ }
+
public static async getCar(tenantID: string, carID: string = Constants.UNKNOWN_STRING_ID, projectFields?: string[]): Promise<Car> {
// Debug
const uniqueTimerID = Logging.traceStart(MODULE_NAME, 'getCar');
@@ -681,4 +781,119 @@ export default class CarStorage {
});
Logging.traceEnd(MODULE_NAME, 'updateDefaultCar', uniqueTimerID, { carID, userID });
}
+
+ public static async getUserCarByCarUser(tenantID: string,
+ carID: string = Constants.UNKNOWN_STRING_ID, userID: string = Constants.UNKNOWN_STRING_ID,
+ projectFields?: string[]): Promise<UserCar> {
+ // Debug
+ const uniqueTimerID = Logging.traceStart(MODULE_NAME, 'getUserCarByCarUser');
+ // Query single Car
+ const usersCarsMDB = await CarStorage.getUsersCars(tenantID,
+ { carIDs: [carID], userIDs: [userID] },
+ Constants.DB_PARAMS_SINGLE_RECORD, projectFields);
+ Logging.traceEnd(MODULE_NAME, 'getUserCarByCarUser', uniqueTimerID, { userID, carID });
+ return usersCarsMDB.count > 0 ? usersCarsMDB.result[0] : null;
+ }
+
+ public static async getUsersCars(tenantID: string,
+ params: { search?: string; userscarsIDs?: string[]; userIDs?: string[]; carIDs?: string[]; active?: boolean } = {},
+ dbParams?: DbParams, projectFields?: string[]): Promise<DataResult<UserCar>> {
+ // Debug
+ const uniqueTimerID = Logging.traceStart(MODULE_NAME, 'getUsersCars');
+ // Check Limit
+ const limit = Utils.checkRecordLimit(dbParams.limit);
+ // Check Skip
+ const skip = Utils.checkRecordSkip(dbParams.skip);
+ // Set the filters
+ const filters: any = {};
+ if (params.active) {
+ filters.active = params.active;
+ }
+ // Limit on Car for Basic Users
+ if (params.userscarsIDs && params.userscarsIDs.length > 0) {
+ // Build filter
+ filters._id = { $in: params.userscarsIDs.map((userscarsID) => Utils.convertToObjectID(userscarsID)) };
+ }
+ if (params.carIDs && params.carIDs.length > 0) {
+ // Build filter
+ filters.carID = { $in: params.carIDs.map((carID) => Utils.convertToObjectID(carID)) };
+ }
+ if (params.userIDs && params.userIDs.length > 0) {
+ // Build filter
+ filters.userID = { $in: params.userIDs.map((userID) => Utils.convertToObjectID(userID)) };
+ }
+ // Create Aggregation
+ const aggregation = [];
+ // Filters
+ if (filters) {
+ aggregation.push({
+ $match: filters
+ });
+ }
+ DatabaseUtils.pushUserLookupInAggregation({
+ tenantID: tenantID, aggregation, localField: 'userID', foreignField: '_id',
+ asField: 'user', oneToOneCardinality: true
+ });
+ // Limit records?
+ if (!dbParams.onlyRecordCount) {
+ // Always limit the nbr of record to avoid perfs issues
+ aggregation.push({ $limit: Constants.DB_RECORD_COUNT_CEIL });
+ }
+ // Count Records
+ const usersCarsCountMDB = await global.database.getCollection<DataResult<UserCar>>(tenantID, 'userscars')
+ .aggregate([...aggregation, { $count: 'count' }], { allowDiskUse: true })
+ .toArray();
+ // Check if only the total count is requested
+ if (dbParams.onlyRecordCount) {
+ // Return only the count
+ return {
+ count: (usersCarsCountMDB.length > 0 ? usersCarsCountMDB[0].count : 0),
+ result: []
+ };
+ }
+ // Remove the limit
+ aggregation.pop();
+ // Convert Object ID to string
+ DatabaseUtils.pushConvertObjectIDToString(aggregation, 'userID');
+ // Add Created By / Last Changed By
+ DatabaseUtils.pushCreatedLastChangedInAggregation(tenantID, aggregation);
+ // Handle the ID
+ DatabaseUtils.pushRenameDatabaseID(aggregation);
+ // Sort
+ if (dbParams.sort) {
+ aggregation.push({
+ $sort: dbParams.sort
+ });
+ } else {
+ aggregation.push({
+ $sort: { name: 1 }
+ });
+ }
+ // Skip
+ if (skip > 0) {
+ aggregation.push({ $skip: skip });
+ }
+ // Limit
+ aggregation.push({
+ $limit: (limit > 0 && limit < Constants.DB_RECORD_COUNT_CEIL) ? limit : Constants.DB_RECORD_COUNT_CEIL
+ });
+ // Project
+ DatabaseUtils.projectFields(aggregation, projectFields);
+ // Read DB
+ const usersCars = await global.database.getCollection<UserCar>(tenantID, 'userscars')
+ .aggregate(aggregation, {
+ collation: { locale: Constants.DEFAULT_LOCALE, strength: 2 },
+ allowDiskUse: true
+ })
+ .toArray();
+ // Debug
+ Logging.traceEnd(MODULE_NAME, 'getUsersCars', uniqueTimerID,
+ { params, limit: dbParams.limit, skip: dbParams.skip, sort: dbParams.sort });
+ // Ok
+ return {
+ count: (usersCarsCountMDB.length > 0 ?
+ (usersCarsCountMDB[0].count === Constants.DB_RECORD_COUNT_CEIL ? -1 : usersCarsCountMDB[0].count) : 0),
+ result: usersCars
+ };
+ }
}

MODIFY
src/storage/mongodb/MongoDBStorage.ts
src/storage/mongodb/MongoDBStorage.ts
@@ -103,7 +103,7 @@ export default class MongoDBStorage {
{ fields: { userID: 1 } }
]);
// User Cars
- await this.handleIndexesInCollection(collections, tenantID, 'usercars', [
+ await this.handleIndexesInCollection(collections, tenantID, 'userscars', [
{ fields: { userID: 1, carID: 1 }, options: { unique: true } }
]);
// Cars

MODIFY
src/storage/mongodb/UserStorage.ts
src/storage/mongodb/UserStorage.ts
@@ -490,7 +490,7 @@ export default class UserStorage {
public static async getUsers(tenantID: string,
params: {
- notificationsActive?: boolean; siteIDs?: string[]; excludeSiteID?: string; search?: string;
+ notificationsActive?: boolean; siteIDs?: string[]; excludeSiteID?: string; search?: string; excludeUserIDs?: string[];
userID?: string; tagID?: string; email?: string; issuer?: boolean; passwordResetHash?: string; roles?: string[];
statuses?: string[]; withImage?: boolean; billingUserID?: string; notSynchronizedBillingData?: boolean;
notifications?: any; noLoginSince?: Date;
@@ -524,6 +524,9 @@ export default class UserStorage {
if (params.issuer === true || params.issuer === false) {
filters.issuer = params.issuer;
}
+ if (params.excludeUserIDs && params.excludeUserIDs.length > 0) {
+ filters._id = { $nin : params.excludeUserIDs.map((userID) => Utils.convertToObjectID(userID)) };
+ }
// Email
if (params.email) {
filters.email = params.email;

MODIFY
src/types/Authorization.ts
src/types/Authorization.ts
@@ -69,6 +69,8 @@ export enum Entity {
CAR_CATALOGS = 'CarCatalogs',
CAR = 'Car',
CARS = 'Cars',
+ USER_CAR='UserCar',
+ USERS_CARS='usersCars',
INVOICE = 'Invoice',
INVOICES = 'Invoices',
TAXES = 'Taxes'

MODIFY
src/types/Car.ts
src/types/Car.ts
@@ -136,6 +136,7 @@ export interface Car extends CreatedUpdatedProps {
carCatalog?: CarCatalog;
userIDs?: string;
users?: User[];
+ type?: CarType;
}
export interface UserCar extends CreatedUpdatedProps {
@@ -145,7 +146,7 @@ export interface UserCar extends CreatedUpdatedProps {
user?: User;
car?: Car;
default?: boolean;
- type?: CarType;
+ active?: boolean;
}
export interface CarMaker {

MODIFY
src/types/Server.ts
src/types/Server.ts
@@ -61,8 +61,13 @@ export enum ServerAction {
CAR_CATALOG_IMAGES = 'CarCatalogImages',
CAR_MAKERS = 'CarMakers',
CAR_CREATE = 'CarCreate',
+ CAR_UPDATE = 'CarUpdate',
+ USER_CAR_UPDATE = 'UserCarUpdate',
CARS = 'Cars',
+ CAR = 'Car',
+ USERS_CAR = 'UsersCar',
SYNCHRONIZE_CAR_CATALOGS = 'SynchronizeCarCatalogs',
+ ASSIGN_USERS_CAR = 'AssignUsersCar',
GET_CONNECTOR_CURRENT_LIMIT = 'GetConnectorCurrentLimit',
REGISTER_USER = 'RegisterUser',

MODIFY
src/types/requests/HttpCarRequest.ts
src/types/requests/HttpCarRequest.ts
-import { CarType } from '../Car';
+import { CarType, UserCar } from '../Car';
+
import HttpDatabaseRequest from './HttpDatabaseRequest';
+
export interface HttpCarCatalogsRequest extends HttpDatabaseRequest {
Search?: string;
CarMaker?: string;
@@ -17,16 +19,32 @@ export interface HttpCarCatalogByIDRequest {
ID: number;
}
+export interface HttpCarByIDRequest {
+ ID: string;
+}
+
export interface HttpCarCreateRequest {
vin: string;
licensePlate: string;
carCatalogID: number;
- forced: boolean;
+ forced?: boolean;
type: CarType;
isDefault: boolean;
+ id?: string;
}
+export interface HttpUsersAssignRequest {
+ carID: string;
+ usersCar: UserCar[];
+}
+
+
export interface HttpCarsRequest extends HttpDatabaseRequest {
Search: string;
CarMaker: string;
}
+
+export interface HttpUsersCarsRequest extends HttpDatabaseRequest {
+ search: string;
+ carID: string;
+}

MODIFY
src/types/requests/HttpUserRequest.ts
src/types/requests/HttpUserRequest.ts
@@ -33,6 +33,7 @@ export interface HttpUsersRequest extends HttpDatabaseRequest {
Status: string;
ErrorType?: string;
ExcludeSiteID: string;
+ ExcludeUserIDs: string;
}
export interface HttpUserSitesRequest extends HttpDatabaseRequest {

Admin Update/Create/Assign/Unassign cars/users,
Manage Lists off line