diff --git a/src/adapters/postgres/user-adapter.ts b/src/adapters/postgres/user-adapter.ts index 925feec..44aacfa 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,137 @@ 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); + } + + 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())