From cbb3a24b22c5fdf82fe33e03fccc69e7e4f38b2d Mon Sep 17 00:00:00 2001 From: vishnu vinay Date: Mon, 3 Mar 2025 17:24:19 +0530 Subject: [PATCH 1/2] TaskId #236323 task: Bulk Upload User API --- src/adapters/postgres/user-adapter.ts | 167 ++++++++++++++++++++++++++ src/common/utils/api-id.config.ts | 1 + src/user/user.controller.ts | 24 ++++ 3 files changed, 192 insertions(+) diff --git a/src/adapters/postgres/user-adapter.ts b/src/adapters/postgres/user-adapter.ts index 925feec..8886533 100644 --- a/src/adapters/postgres/user-adapter.ts +++ b/src/adapters/postgres/user-adapter.ts @@ -35,6 +35,7 @@ import { JwtUtil } from '@utils/jwt-token'; import { ConfigService } from '@nestjs/config'; import { formatTime } from '@utils/formatTimeConversion'; import { API_RESPONSES } from '@utils/response.messages'; +import { parse } from 'csv-parse/sync'; @Injectable() @@ -952,6 +953,172 @@ export class PostgresUserService implements IServicelocator { return APIResponse.error(response, apiId, "Internal Server Error", errorMessage, HttpStatus.INTERNAL_SERVER_ERROR); } } + async bulkCreateUser(request: any, userCreateDto: UserCreateDto, response: Response) { + // check and validate all fields + let validatedRoles = await this.validateRequestBody(userCreateDto) + + if ( + validatedRoles && + Array.isArray(validatedRoles) && + validatedRoles.length > 0 + ) { + const errorMessage = validatedRoles.join("; "); + throw new Error(errorMessage); + } else if (validatedRoles) { + throw new Error("Validation Error"); + } + + userCreateDto.username = userCreateDto.username.toLocaleLowerCase(); + const userSchema = new UserCreateDto(userCreateDto); + + let errKeycloak = ""; + let resKeycloak = ""; + + const keycloakResponse = await getKeycloakAdminToken(); + const token = keycloakResponse.data.access_token; + let checkUserinKeyCloakandDb = await this.checkUserinKeyCloakandDb(userCreateDto) + // let checkUserinDb = await this.checkUserinKeyCloakandDb(userCreateDto.username); + if (checkUserinKeyCloakandDb) { + throw new Error('User Already Exist') + } + resKeycloak = await createUserInKeyCloak(userSchema, token).catch( + (error) => { + errKeycloak = error.response?.data.errorMessage; + throw new Error(errKeycloak); + } + ); + + userCreateDto.userId = resKeycloak; + + let result = await this.createUserInDatabase(request, userCreateDto, response); + + const createFailures = []; + if (result && userCreateDto.customFields && userCreateDto.customFields.length > 0) { + + let userId = result?.userId; + let roles; + + if (validatedRoles) { + roles = validatedRoles?.map(({ code }) => code?.toUpperCase()) + } + + const customFields = await this.fieldsService.findCustomFields("USERS", roles) + + if (customFields) { + const customFieldAttributes = customFields.reduce((fieldDetail, { fieldId, fieldAttributes, fieldParams, name }) => fieldDetail[`${fieldId}`] ? fieldDetail : { ...fieldDetail, [`${fieldId}`]: { fieldAttributes, fieldParams, name } }, {}); + + + for (let fieldValues of userCreateDto.customFields) { + const fieldData = { + fieldId: fieldValues['fieldId'], + value: fieldValues['value'] + } + + let res = await this.fieldsService.updateCustomFields(userId, fieldData, customFieldAttributes[fieldData.fieldId]); + + if (res.correctValue) { + if (!result['customFields']) + result['customFields'] = []; + result["customFields"].push(res); + } else { + createFailures.push(`${fieldData.fieldId}: ${res?.valueIssue} - ${res.fieldName}`) + } + } + } + } + } + + async bulkUploadUsers(request: any,csvFile :any, response: Response) { + const apiId = APIID.USER_CREATE_BULK; + const decoded: any = jwt_decode(request.headers.authorization); + + try { + if (csvFile == undefined) { + return APIResponse.error( + response, + apiId, + "BAD_REQUEST", + "CSV file is required", + HttpStatus.BAD_REQUEST + ); + } + const csvData = csvFile.buffer.toString('utf8'); + + // Parse CSV data + const records = parse(csvData, { + columns: true, + skip_empty_lines: true, + trim: true + }); + + const results = { + success: 0, + failed: 0, + failedDetails: [] + }; + + // Process each user record + for (const record of records) { + // Validate required fields + if (!record.username || !record.name || !record.password) { + results.failed++; + results.failedDetails.push({ + record: record.username || 'Unknown', + error: 'Missing required fields (username, name, or password)' + }); + continue; + } + + // Create UserCreateDto from CSV record + let userCreateDto = + { + username :record.username.toLocaleLowerCase(), + name : record.name, + password : record.password, + createdBy : decoded.sub, + updatedBy : decoded.sub, + tenantCohortRoleMapping : [ + { + roleId: request.body.roleId, + tenantId: request.body.tenantId, + cohortId: [request.body.cohortId] + } + ] + } + const userSchema = new UserCreateDto(userCreateDto); + try { + await this.bulkCreateUser(request, userSchema, response); + results.success++; + } + catch (error) { + results.failed++; + results.failedDetails.push({ + record: record.username || 'Unknown', + error : error.message + }); + } + } + + // Return overall results + return APIResponse.success( + response, + apiId, + results, + HttpStatus.OK, + `Bulk upload completed. Success: ${results.success}, Failed: ${results.failed}` + ); + + } catch (e) { + const errorMessage = e.message || 'Internal server error'; + return APIResponse.error( + response, + apiId, + "Internal Server Error", + errorMessage, + HttpStatus.INTERNAL_SERVER_ERROR + ); + } + } async validateRequestBody(userCreateDto) { diff --git a/src/common/utils/api-id.config.ts b/src/common/utils/api-id.config.ts index b9d386c..caa5783 100644 --- a/src/common/utils/api-id.config.ts +++ b/src/common/utils/api-id.config.ts @@ -1,6 +1,7 @@ export const APIID = { USER_GET: "api.user.get", USER_CREATE: "api.user.create", + USER_CREATE_BULK : "api.user.bulkCreate", USER_UPDATE: "api.user.update", USER_LIST: "api.user.list", USER_RESET_PASSWORD: "api.user.resetPassword", diff --git a/src/user/user.controller.ts b/src/user/user.controller.ts index 3e599cc..78f1b5f 100644 --- a/src/user/user.controller.ts +++ b/src/user/user.controller.ts @@ -16,6 +16,8 @@ import { Delete, ParseUUIDPipe, UseFilters, + UploadedFile, + UseInterceptors } from "@nestjs/common"; import { @@ -31,6 +33,7 @@ import { ApiInternalServerErrorResponse, ApiBadRequestResponse, ApiConflictResponse, + ApiConsumes, } from "@nestjs/swagger"; import { UserSearchDto } from "./dto/user-search.dto"; @@ -43,6 +46,7 @@ import { AllExceptionsFilter } from "src/common/filters/exception.filter"; import { APIID } from "src/common/utils/api-id.config"; import { ForgotPasswordDto, ResetUserPasswordDto, SendPasswordResetLinkDto,learnerForgotPasswordDto } from "./dto/passwordReset.dto"; import { PostgresUserService } from "src/adapters/postgres/user-adapter"; +import { FileInterceptor } from '@nestjs/platform-express'; export interface UserData { context: string; @@ -115,6 +119,26 @@ export class UserController { } + // bulk create of users + @UseInterceptors(FileInterceptor('csvFile')) + @UseFilters(new AllExceptionsFilter(APIID.USER_CREATE_BULK)) + @Post("/bulk-create") + @UseGuards(JwtAuthGuard) + @ApiBasicAuth("access-token") + @ApiConsumes('multipart/form-data') + @ApiCreatedResponse({ description: "Users have been created successfully." }) + @ApiForbiddenResponse({ description: "Forbidden" }) + @ApiHeader({ + name: "tenantid", + }) + public async bulkCreateUsers( + @Req() request: Request, + @UploadedFile() csvFile: Express.Multer.File, + @Res() response: Response + ) { + return await this.postgresUserService.bulkUploadUsers(request, csvFile, response) + } + @UseFilters(new AllExceptionsFilter(APIID.USER_UPDATE)) @Patch("update/:userid") @UsePipes(new ValidationPipe()) From 631d127f93012d73212107f289655687cc202a0e Mon Sep 17 00:00:00 2001 From: vishnu vinay Date: Mon, 3 Mar 2025 18:01:49 +0530 Subject: [PATCH 2/2] Fix : Duplicatecode for bulk upload --- src/adapters/postgres/user-adapter.ts | 97 +++++++++------------------ 1 file changed, 31 insertions(+), 66 deletions(-) diff --git a/src/adapters/postgres/user-adapter.ts b/src/adapters/postgres/user-adapter.ts index 8886533..44aacfa 100644 --- a/src/adapters/postgres/user-adapter.ts +++ b/src/adapters/postgres/user-adapter.ts @@ -954,78 +954,43 @@ export class PostgresUserService implements IServicelocator { } } async bulkCreateUser(request: any, userCreateDto: UserCreateDto, response: Response) { - // check and validate all fields - let validatedRoles = await this.validateRequestBody(userCreateDto) - - if ( - validatedRoles && - Array.isArray(validatedRoles) && - validatedRoles.length > 0 - ) { - const errorMessage = validatedRoles.join("; "); - throw new Error(errorMessage); - } else if (validatedRoles) { - throw new Error("Validation Error"); - } + // check and validate all fields + let validatedRoles = await this.validateRequestBody(userCreateDto) + + if ( + validatedRoles && + Array.isArray(validatedRoles) && + validatedRoles.length > 0 + ) { + const errorMessage = validatedRoles.join("; "); + throw new Error(errorMessage); + } else if (validatedRoles) { + throw new Error("Validation Error"); + } - userCreateDto.username = userCreateDto.username.toLocaleLowerCase(); - const userSchema = new UserCreateDto(userCreateDto); + userCreateDto.username = userCreateDto.username.toLocaleLowerCase(); + const userSchema = new UserCreateDto(userCreateDto); - let errKeycloak = ""; - let resKeycloak = ""; + let errKeycloak = ""; + let resKeycloak = ""; - const keycloakResponse = await getKeycloakAdminToken(); - const token = keycloakResponse.data.access_token; - let checkUserinKeyCloakandDb = await this.checkUserinKeyCloakandDb(userCreateDto) - // let checkUserinDb = await this.checkUserinKeyCloakandDb(userCreateDto.username); - if (checkUserinKeyCloakandDb) { - throw new Error('User Already Exist') + const keycloakResponse = await getKeycloakAdminToken(); + const token = keycloakResponse.data.access_token; + let checkUserinKeyCloakandDb = await this.checkUserinKeyCloakandDb(userCreateDto) + // let checkUserinDb = await this.checkUserinKeyCloakandDb(userCreateDto.username); + if (checkUserinKeyCloakandDb) { + throw new Error('User Already Exist') + } + resKeycloak = await createUserInKeyCloak(userSchema, token).catch( + (error) => { + errKeycloak = error.response?.data.errorMessage; + throw new Error(errKeycloak); } - resKeycloak = await createUserInKeyCloak(userSchema, token).catch( - (error) => { - errKeycloak = error.response?.data.errorMessage; - throw new Error(errKeycloak); - } - ); - - userCreateDto.userId = resKeycloak; - - let result = await this.createUserInDatabase(request, userCreateDto, response); - - const createFailures = []; - if (result && userCreateDto.customFields && userCreateDto.customFields.length > 0) { - - let userId = result?.userId; - let roles; - - if (validatedRoles) { - roles = validatedRoles?.map(({ code }) => code?.toUpperCase()) - } - - const customFields = await this.fieldsService.findCustomFields("USERS", roles) - - if (customFields) { - const customFieldAttributes = customFields.reduce((fieldDetail, { fieldId, fieldAttributes, fieldParams, name }) => fieldDetail[`${fieldId}`] ? fieldDetail : { ...fieldDetail, [`${fieldId}`]: { fieldAttributes, fieldParams, name } }, {}); - - - for (let fieldValues of userCreateDto.customFields) { - const fieldData = { - fieldId: fieldValues['fieldId'], - value: fieldValues['value'] - } + ); - let res = await this.fieldsService.updateCustomFields(userId, fieldData, customFieldAttributes[fieldData.fieldId]); + userCreateDto.userId = resKeycloak; - if (res.correctValue) { - if (!result['customFields']) - result['customFields'] = []; - result["customFields"].push(res); - } else { - createFailures.push(`${fieldData.fieldId}: ${res?.valueIssue} - ${res.fieldName}`) - } - } - } - } + let result = await this.createUserInDatabase(request, userCreateDto, response); } async bulkUploadUsers(request: any,csvFile :any, response: Response) {