implemented import async task with assign tag to user
addded utils to handle csv values inside rows (double quotes)
MODIFY
src/async-task/tasks/TagsImportAsyncTask.ts
src/async-task/tasks/TagsImportAsyncTask.ts
import { ActionsResponse, ImportStatus } from '../../types/GlobalType';
import Tag, { ImportedTag } from '../../types/Tag';
+import User, { UserRole, UserStatus } from '../../types/User';
import AbstractAsyncTask from '../AsyncTask';
import Constants from '../../utils/Constants';
@@ -10,7 +11,9 @@ import LockingManager from '../../locking/LockingManager';
import Logging from '../../utils/Logging';
import { ServerAction } from '../../types/Server';
import TagStorage from '../../storage/mongodb/TagStorage';
+import Tenant from '../../types/Tenant';
import TenantStorage from '../../storage/mongodb/TenantStorage';
+import UserStorage from '../../storage/mongodb/UserStorage';
import Utils from '../../utils/Utils';
const MODULE_NAME = 'TagsImportAsyncTask';
@@ -59,6 +62,10 @@ export default class TagsImportAsyncTask extends AbstractAsyncTask {
if (foundTag.transactionsCount > 0) {
throw new Error(`Tag is already used in ${foundTag.transactionsCount} transaction(s)`);
}
+ // Save user if any and get the ID to assign tag
+ if (importedTag.email && importedTag.name && importedTag.firstName) {
+ await this.assignTag(tenant, importedTag, foundTag);
+ }
// Update it
await TagStorage.saveTag(tenant.id, { ...foundTag, ...importedTag });
// Remove the imported Tag
@@ -75,6 +82,10 @@ export default class TagsImportAsyncTask extends AbstractAsyncTask {
createdBy: { id: importedTag.importedBy },
createdOn: importedTag.importedOn,
};
+ // Save user if any and get the ID to assign tag
+ if (importedTag.email && importedTag.name && importedTag.firstName) {
+ await this.assignTag(tenant, importedTag, tag);
+ }
// Save the new Tag
await TagStorage.saveTag(tenant.id, tag);
// Remove the imported Tag
@@ -125,4 +136,27 @@ export default class TagsImportAsyncTask extends AbstractAsyncTask {
}
}
}
+
+ private async assignTag(tenant: Tenant, importedTag: ImportedTag, tag: Tag) {
+ // Save user if any and get the ID to assign tag
+ const foundUser = await UserStorage.getUserByEmail(tenant.id,importedTag.email);
+ if (!foundUser) {
+ const user = {
+ name: importedTag.name,
+ firstName: importedTag.firstName,
+ email: importedTag.email,
+ createdBy: { id: importedTag.importedBy },
+ createdOn: Utils.convertToDate(importedTag.importedOn),
+ issuer: true,
+ status: UserStatus.PENDING,
+ role: UserRole.BASIC,
+ } as User;
+ const userID = await UserStorage.saveUser(tenant.id, user);
+ await UserStorage.saveUserStatus(tenant.id, userID, user.status);
+ await UserStorage.saveUserRole(tenant.id, userID, user.role);
+ tag.userID = userID;
+ } else {
+ tag.userID = foundUser.id;
+ }
+ }
}

MODIFY
src/server/rest/v1/service/ChargingStationService.ts
src/server/rest/v1/service/ChargingStationService.ts
@@ -1544,7 +1544,7 @@ export default class ChargingStationService {
Utils.replaceSpecialCharsInCSVValueParam(param.value),
ocppParams.siteAreaName,
ocppParams.siteName,
- ].map((value) => Utils.replaceDoubleQuotes(value));
+ ].map((value) => Utils.escapeCsvValues(value));
return row;
}).join(Constants.CR_LF);
return Utils.isNullOrUndefined(headers) ? Constants.CR_LF + rows : [headers, rows].join(Constants.CR_LF);
@@ -1608,7 +1608,7 @@ export default class ChargingStationService {
i18nManager.formatDateTime(chargingStation.lastReboot, 'L') + ' ' + i18nManager.formatDateTime(chargingStation.lastReboot, 'LT'),
chargingStation.maximumPower,
chargingStation.powerLimitUnit
- ].map((value) => Utils.replaceDoubleQuotes(value));
+ ].map((value) => Utils.escapeCsvValues(value));
return row;
}).join(Constants.CR_LF);
return Utils.isNullOrUndefined(headers) ? Constants.CR_LF + rows : [headers, rows].join(Constants.CR_LF);

MODIFY
src/server/rest/v1/service/LoggingService.ts
src/server/rest/v1/service/LoggingService.ts
@@ -85,7 +85,7 @@ export default class LoggingService {
log.source,
log.host,
log.process
- ].map((value) => Utils.replaceDoubleQuotes(value));
+ ].map((value) => Utils.escapeCsvValues(value));
return row;
}).join(Constants.CR_LF);
return Utils.isNullOrUndefined(headers) ? Constants.CR_LF + rows : [headers, rows].join(Constants.CR_LF);

MODIFY
src/server/rest/v1/service/StatisticService.ts
src/server/rest/v1/service/StatisticService.ts
@@ -652,7 +652,7 @@ export default class StatisticService {
year && year !== '0' ? year : '',
transaction.month > 0 ? transaction.month : '',
dataType === StatsDataType.PRICING ? StatisticService.getPricingCell(transaction, numberOfTransactions) : numberOfTransactions.toString()
- ].map((value) => Utils.replaceDoubleQuotes(value));
+ ].map((value) => Utils.escapeCsvValues(value));
return row;
}).join(Constants.CR_LF);
return [headers, rows].join(Constants.CR_LF);

MODIFY
src/server/rest/v1/service/TagService.ts
src/server/rest/v1/service/TagService.ts
@@ -15,6 +15,7 @@ import CSVError from 'csvtojson/v2/CSVError';
import Constants from '../../../../utils/Constants';
import { DataResult } from '../../../../types/DataResult';
import EmspOCPIClient from '../../../../client/ocpi/EmspOCPIClient';
+import { ImportedUser } from '../../../../types/User';
import JSONStream from 'JSONStream';
import LockingHelper from '../../../../locking/LockingHelper';
import LockingManager from '../../../../locking/LockingManager';
@@ -33,6 +34,7 @@ import TenantStorage from '../../../../storage/mongodb/TenantStorage';
import TransactionStorage from '../../../../storage/mongodb/TransactionStorage';
import UserStorage from '../../../../storage/mongodb/UserStorage';
import UserToken from '../../../../types/UserToken';
+import UserValidator from '../validator/UserValidator';
import Utils from '../../../../utils/Utils';
import UtilsService from './UtilsService';
import csvToJson from 'csvtojson/v2';
@@ -430,7 +432,6 @@ export default class TagService {
trim: true,
delimiter: Constants.CSV_SEPARATOR,
output: 'json',
- quote: 'on',
});
void converter.subscribe(async (tag: ImportedTag) => {
// Check connection
@@ -458,7 +459,7 @@ export default class TagService {
result.inError++;
}
// Insert batched
- if ((tagsToBeImported.length % Constants.IMPORT_BATCH_INSERT_SIZE) === 0) {
+ if (!Utils.isEmptyArray(tagsToBeImported) && (tagsToBeImported.length % Constants.IMPORT_BATCH_INSERT_SIZE) === 0) {
await TagService.insertTags(req.user.tenantID, req.user, action, tagsToBeImported, result);
}
// eslint-disable-next-line @typescript-eslint/no-misused-promises
@@ -713,7 +714,7 @@ export default class TagService {
tag.user?.firstName,
tag.user?.name,
tag.user?.email
- ].map((value) => typeof value === 'string' ? '"' + value.replace(/^"|"$/g, '') + '"' : value);
+ ].map((value) => Utils.escapeCsvValues(value));
return row;
}).join(Constants.CR_LF);
return Utils.isNullOrUndefined(headers) ? Constants.CR_LF + rows : [headers, rows].join(Constants.CR_LF);
@@ -760,7 +761,10 @@ export default class TagService {
try {
const newImportedTag: ImportedTag = {
id: importedTag.id.toUpperCase(),
- description: importedTag.description ? importedTag.description : `Badge ID '${importedTag.id}'`,
+ description: importedTag.description ? Utils.escapeCsvValues(importedTag.description) : `Badge ID '${importedTag.id}'`,
+ name: Utils.escapeCsvValues(importedTag.name.toUpperCase()),
+ firstName: Utils.escapeCsvValues(importedTag.firstName),
+ email: Utils.escapeCsvValues(importedTag.email),
};
// Validate Tag data
TagValidator.getInstance().validateImportedTagCreation(newImportedTag);
@@ -768,6 +772,13 @@ export default class TagService {
newImportedTag.importedBy = importedTag.importedBy;
newImportedTag.importedOn = importedTag.importedOn;
newImportedTag.status = ImportStatus.READY;
+ try {
+ UserValidator.getInstance().validateImportedUserCreation(newImportedTag as ImportedUser);
+ } catch (error) {
+ newImportedTag.email = '';
+ newImportedTag.name = '';
+ newImportedTag.firstName = '';
+ }
// Save it later on
tagsToBeImported.push(newImportedTag);
return true;

MODIFY
src/server/rest/v1/service/TransactionService.ts
src/server/rest/v1/service/TransactionService.ts
@@ -929,7 +929,7 @@ export default class TransactionService {
(transaction.stop.totalInactivitySecs ? Utils.truncTo(Utils.createDecimal(transaction.stop.totalInactivitySecs).div(60).toNumber(), 2) : 0) : '',
transaction.stop ? transaction.stop.roundedPrice : '',
transaction.stop ? transaction.stop.priceUnit : ''
- ].map((value) => Utils.replaceDoubleQuotes(value));
+ ].map((value) => Utils.escapeCsvValues(value));
return row;
}).join(Constants.CR_LF);
return Utils.isNullOrUndefined(headers) ? Constants.CR_LF + rows : [headers, rows].join(Constants.CR_LF);

MODIFY
src/server/rest/v1/service/UserService.ts
src/server/rest/v1/service/UserService.ts
@@ -1277,7 +1277,7 @@ export default class UserService {
moment(user.createdOn).format('YYYY-MM-DD'),
moment(user.lastChangedOn).format('YYYY-MM-DD'),
(user.lastChangedBy ? Utils.buildUserFullName(user.lastChangedBy as User, false) : '')
- ].map((value) => Utils.replaceDoubleQuotes(value));
+ ].map((value) => Utils.escapeCsvValues(value));
return row;
}).join(Constants.CR_LF);
return Utils.isNullOrUndefined(headers) ? Constants.CR_LF + rows : [headers, rows].join(Constants.CR_LF);
@@ -1342,9 +1342,9 @@ export default class UserService {
private static async processUser(action: ServerAction, req: Request, importedUser: ImportedUser, usersToBeImported: ImportedUser[]): Promise<boolean> {
try {
const newImportedUser: ImportedUser = {
- name: importedUser.name.toUpperCase().replace(/^"|"$/g, ''),
- firstName: importedUser.firstName.replace(/^"|"$/g, ''),
- email: importedUser.email.replace(/^"|"$/g, ''),
+ name: Utils.escapeCsvValues(importedUser.name),
+ firstName: Utils.escapeCsvValues(importedUser.firstName),
+ email: Utils.escapeCsvValues(importedUser.email),
};
// Validate User data

MODIFY
src/storage/mongodb/TagStorage.ts
src/storage/mongodb/TagStorage.ts
@@ -42,7 +42,10 @@ export default class TagStorage {
const uniqueTimerID = Logging.traceStart(tenantID, MODULE_NAME, 'saveImportedTag');
const tagMDB = {
_id: importedTagToSave.id,
- description: importedTagToSave.description,
+ description: Utils.unEscapeCsvValues(importedTagToSave.description),
+ name: Utils.unEscapeCsvValues(importedTagToSave.name),
+ firstName: Utils.unEscapeCsvValues(importedTagToSave.firstName),
+ email: Utils.unEscapeCsvValues(importedTagToSave.email),
status: importedTagToSave.status,
errorDescription: importedTagToSave.errorDescription,
importedOn: Utils.convertToDate(importedTagToSave.importedOn),
@@ -62,7 +65,10 @@ export default class TagStorage {
const uniqueTimerID = Logging.traceStart(tenantID, MODULE_NAME, 'saveImportedTags');
const importedTagsToSaveMDB: any = importedTagsToSave.map((importedTagToSave) => ({
_id: importedTagToSave.id,
- description: importedTagToSave.description,
+ description: Utils.unEscapeCsvValues(importedTagToSave.description),
+ name: Utils.unEscapeCsvValues(importedTagToSave.name),
+ firstName: Utils.unEscapeCsvValues(importedTagToSave.firstName),
+ email: Utils.unEscapeCsvValues(importedTagToSave.email),
status: importedTagToSave.status,
errorDescription: importedTagToSave.errorDescription,
importedOn: Utils.convertToDate(importedTagToSave.importedOn),

MODIFY
src/types/Tag.ts
src/types/Tag.ts
@@ -22,6 +22,9 @@ export interface ImportedTag {
importedOn?: Date;
status?: ImportStatus;
errorDescription?: string;
+ name?: string;
+ firstName?: string;
+ email?: string;
}
export const TagRequiredImportProperties = [

MODIFY
src/utils/Utils.ts
src/utils/Utils.ts
@@ -1444,7 +1444,14 @@ export default class Utils {
return Configuration.isCloudFoundry() ? cfenv.getAppEnv().name : os.hostname();
}
- public static replaceDoubleQuotes(value: any): string {
+ public static escapeCsvValues(value: any): string {
+ // replace starting and ending double quotes by nothing
+ // replace double quotes inside value to double double quotes to display double quote in csv editor
return typeof value === 'string' ? '"' + value.replace(/^"|"$/g, '').replace(/"/g, '""') + '"' : value;
}
+
+ public static unEscapeCsvValues(value: any): string {
+ // replace double double quotes inside value into simple double quotes
+ return typeof value === 'string' ? value.replace(/""/g, '"') : value;
+ }
}

implemented import async task with assign tag to user
addded utils to handle csv values inside rows (double quotes)