From f29315f1335333f1f710a59dfc209d9d38f3946f Mon Sep 17 00:00:00 2001 From: tipusinghaw Date: Tue, 7 Nov 2023 11:21:37 +0530 Subject: [PATCH 01/62] feat: fetch file details for bulk operation Signed-off-by: tipusinghaw --- .../src/issuance/dtos/issuance.dto.ts | 30 ++++ .../src/issuance/interfaces/index.ts | 2 +- .../src/issuance/issuance.controller.ts | 128 +++++++++++++++++- .../src/issuance/issuance.service.ts | 26 +++- .../interfaces/issuance.interfaces.ts | 10 +- apps/issuance/src/issuance.controller.ts | 16 +++ apps/issuance/src/issuance.repository.ts | 84 +++++++++++- apps/issuance/src/issuance.service.ts | 59 ++++++++ libs/common/src/response-messages/index.ts | 3 +- 9 files changed, 348 insertions(+), 10 deletions(-) diff --git a/apps/api-gateway/src/issuance/dtos/issuance.dto.ts b/apps/api-gateway/src/issuance/dtos/issuance.dto.ts index 148f65f57..a024e3591 100644 --- a/apps/api-gateway/src/issuance/dtos/issuance.dto.ts +++ b/apps/api-gateway/src/issuance/dtos/issuance.dto.ts @@ -175,4 +175,34 @@ export class PreviewFileDetails { @Type(() => String) sortValue = ''; +} + +export class FileParameter { + @ApiProperty({ required: false, default: 1 }) + @IsOptional() + @Type(() => Number) + @Transform(({ value }) => toNumber(value)) + pageNumber = 1; + + @ApiProperty({ required: false }) + @IsOptional() + @Type(() => String) + search = ''; + + @ApiProperty({ required: false, default: 10 }) + @IsOptional() + @Type(() => Number) + @Transform(({ value }) => toNumber(value)) + pageSize = 10; + + @ApiProperty({ required: false }) + @IsOptional() + @Type(() => String) + sortBy = ''; + + @ApiProperty({ required: false }) + @IsOptional() + @Type(() => String) + sortValue = ''; + } \ No newline at end of file diff --git a/apps/api-gateway/src/issuance/interfaces/index.ts b/apps/api-gateway/src/issuance/interfaces/index.ts index b6c8b0307..15d3db8f3 100644 --- a/apps/api-gateway/src/issuance/interfaces/index.ts +++ b/apps/api-gateway/src/issuance/interfaces/index.ts @@ -66,4 +66,4 @@ export interface RequestPayload { credDefId: string; filePath: string; fileName: string; - } \ No newline at end of file + } diff --git a/apps/api-gateway/src/issuance/issuance.controller.ts b/apps/api-gateway/src/issuance/issuance.controller.ts index bf3310e93..8ae2dda81 100644 --- a/apps/api-gateway/src/issuance/issuance.controller.ts +++ b/apps/api-gateway/src/issuance/issuance.controller.ts @@ -38,7 +38,7 @@ import { CommonService } from '@credebl/common/common.service'; import { Response } from 'express'; import IResponseType from '@credebl/common/interfaces/response.interface'; import { IssuanceService } from './issuance.service'; -import { IssuanceDto, IssueCredentialDto, OutOfBandCredentialDto, PreviewFileDetails } from './dtos/issuance.dto'; +import { FileParameter, IssuanceDto, IssueCredentialDto, OutOfBandCredentialDto, PreviewFileDetails } from './dtos/issuance.dto'; import { IUserRequest } from '@credebl/user-request/user-request.interface'; import { User } from '../authz/decorators/user.decorator'; import { ResponseMessages } from '@credebl/common/response-messages'; @@ -331,6 +331,132 @@ export class IssuanceController { return res.status(HttpStatus.OK).json(finalResponse); } + @Get('/orgs/:orgId/bulk/files') + @Roles(OrgRoles.OWNER, OrgRoles.ADMIN, OrgRoles.ISSUER, OrgRoles.VERIFIER) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + @ApiBearerAuth() + @ApiResponse({ status: 200, description: 'Success', type: ApiResponseDto }) + @ApiUnauthorizedResponse({ + status: 401, + description: 'Unauthorized', + type: UnauthorizedErrorDto + }) + @ApiForbiddenResponse({ + status: 403, + description: 'Forbidden', + type: ForbiddenErrorDto + }) + @ApiOperation({ + summary: 'Get the file list for bulk operation', + description: 'Get all the file list for organization for bulk operation' + }) + + @ApiQuery({ + name: 'pageNumber', + type: Number, + required: false + }) + @ApiQuery({ + name: 'search', + type: String, + required: false + }) + @ApiQuery({ + name: 'pageSize', + type: Number, + required: false + }) + @ApiQuery({ + name: 'sortBy', + type: String, + required: false + }) + @ApiQuery({ + name: 'sortValue', + type: Number, + required: false + }) + async issuedFileDetails( + @Param('orgId') orgId: number, + @Query() fileParameter: FileParameter, + @Res() res: Response + ): Promise { + const issuedFileDetails = await this.issueCredentialService.issuedFileDetails( + orgId, + fileParameter + ); + const finalResponse: IResponseType = { + statusCode: HttpStatus.OK, + message: ResponseMessages.issuance.success.previewCSV, + data: issuedFileDetails.response + }; + return res.status(HttpStatus.OK).json(finalResponse); + } + + @Get('/orgs/:orgId/bulk/file-data') + @Roles(OrgRoles.OWNER, OrgRoles.ADMIN, OrgRoles.ISSUER, OrgRoles.VERIFIER) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + @ApiBearerAuth() + @ApiResponse({ status: 200, description: 'Success', type: ApiResponseDto }) + @ApiUnauthorizedResponse({ + status: 401, + description: 'Unauthorized', + type: UnauthorizedErrorDto + }) + @ApiForbiddenResponse({ + status: 403, + description: 'Forbidden', + type: ForbiddenErrorDto + }) + @ApiOperation({ + summary: 'Get the file data', + description: 'Get the file data by file id' + }) + + @ApiQuery({ + name: 'pageNumber', + type: Number, + required: false + }) + @ApiQuery({ + name: 'search', + type: String, + required: false + }) + @ApiQuery({ + name: 'pageSize', + type: Number, + required: false + }) + @ApiQuery({ + name: 'sortBy', + type: String, + required: false + }) + @ApiQuery({ + name: 'sortValue', + type: Number, + required: false + }) + async getFileDetailsByFileId( + @Param('orgId') orgId: number, + @Param('fileId') fileId: string, + @Query() fileParameter: FileParameter, + @Res() res: Response + ): Promise { + const issuedFileDetails = await this.issueCredentialService.getFileDetailsByFileId( + orgId, + fileId, + fileParameter + ); + const finalResponse: IResponseType = { + statusCode: HttpStatus.OK, + message: ResponseMessages.issuance.success.previewCSV, + data: issuedFileDetails.response + }; + return res.status(HttpStatus.OK).json(finalResponse); + } + @Post('/orgs/:orgId/:requestId/bulk') @Roles(OrgRoles.OWNER, OrgRoles.ADMIN, OrgRoles.ISSUER, OrgRoles.VERIFIER) @UseGuards(AuthGuard('jwt'), OrgRolesGuard) diff --git a/apps/api-gateway/src/issuance/issuance.service.ts b/apps/api-gateway/src/issuance/issuance.service.ts index fae42c4ed..785292794 100644 --- a/apps/api-gateway/src/issuance/issuance.service.ts +++ b/apps/api-gateway/src/issuance/issuance.service.ts @@ -3,7 +3,7 @@ import { Injectable, Inject } from '@nestjs/common'; import { ClientProxy } from '@nestjs/microservices'; import { BaseService } from 'libs/service/base.service'; import { IUserRequest } from '@credebl/user-request/user-request.interface'; -import { IssuanceDto, IssueCredentialDto, OutOfBandCredentialDto, PreviewFileDetails } from './dtos/issuance.dto'; +import { FileParameter, IssuanceDto, IssueCredentialDto, OutOfBandCredentialDto, PreviewFileDetails } from './dtos/issuance.dto'; import { FileExportResponse, RequestPayload } from './interfaces'; @Injectable() @@ -83,6 +83,30 @@ export class IssuanceService extends BaseService { return this.sendNats(this.issuanceProxy, 'preview-csv-details', payload); } + async issuedFileDetails( + orgId: number, + fileParameter: FileParameter + ): Promise<{ response: object }> { + const payload = { + orgId, + fileParameter + }; + return this.sendNats(this.issuanceProxy, 'issued-file-details', payload); + } + + async getFileDetailsByFileId( + orgId: number, + fileId: string, + fileParameter: FileParameter + ): Promise<{ response: object }> { + const payload = { + orgId, + fileId, + fileParameter + }; + return this.sendNats(this.issuanceProxy, 'issued-file-data', payload); + } + async issueBulkCredential(requestId: string, orgId: number): Promise<{ response: object }> { const payload = { requestId, orgId }; return this.sendNats(this.issuanceProxy, 'issue-bulk-credentials', payload); diff --git a/apps/issuance/interfaces/issuance.interfaces.ts b/apps/issuance/interfaces/issuance.interfaces.ts index 9ea1d37c1..80e6ad314 100644 --- a/apps/issuance/interfaces/issuance.interfaces.ts +++ b/apps/issuance/interfaces/issuance.interfaces.ts @@ -76,11 +76,11 @@ export interface ImportFileDetails { } export interface PreviewRequest { - pageNumber: number, - search: string, - pageSize: number, - sortBy: string, - sortValue: string + pageNumber?: number, + search?: string, + pageSize?: number, + sortBy?: string, + sortValue?: string } export interface FileUpload { diff --git a/apps/issuance/src/issuance.controller.ts b/apps/issuance/src/issuance.controller.ts index 03594a208..f11a148b9 100644 --- a/apps/issuance/src/issuance.controller.ts +++ b/apps/issuance/src/issuance.controller.ts @@ -66,6 +66,22 @@ export class IssuanceController { ); } + @MessagePattern({ cmd: 'issued-file-details' }) + async issuedFiles(payload: {orgId:string, fileParameter:PreviewRequest}): Promise { + return this.issuanceService.issuedFileDetails( + payload.orgId, + payload.fileParameter + ); + } + @MessagePattern({ cmd: 'issued-file-data' }) + async getFileDetailsByFileId(payload: {fileId:string, fileParameter:PreviewRequest}): Promise { + return this.issuanceService.getFileDetailsByFileId( + payload.fileId, + payload.fileParameter + ); + } + + @MessagePattern({ cmd: 'issue-bulk-credentials' }) async issueBulkCredentials(payload: {requestId:string, orgId:number }): Promise { return this.issuanceService.issueBulkCredential(payload.requestId, payload.orgId); diff --git a/apps/issuance/src/issuance.repository.ts b/apps/issuance/src/issuance.repository.ts index a181e92bc..2880f26d0 100644 --- a/apps/issuance/src/issuance.repository.ts +++ b/apps/issuance/src/issuance.repository.ts @@ -4,7 +4,7 @@ import { PrismaService } from '@credebl/prisma-service'; // eslint-disable-next-line camelcase import { agent_invitations, credentials, file_data, file_upload, org_agents, organisation, platform_config, shortening_url } from '@prisma/client'; import { ResponseMessages } from '@credebl/common/response-messages'; -import { FileUpload, FileUploadData, SchemaDetails } from '../interfaces/issuance.interfaces'; +import { FileUpload, FileUploadData, PreviewRequest, SchemaDetails } from '../interfaces/issuance.interfaces'; @Injectable() export class IssuanceRepository { @@ -237,6 +237,88 @@ export class IssuanceRepository { } } + async getAllFileDetails(orgId: string, getAllfileDetails: PreviewRequest): Promise<{ + fileCount: number + fileList: { + id: string; + name: string; + status: string; + upload_type: string; + orgId: string; + createDateTime: Date; + createdBy: string; + lastChangedDateTime: Date; + lastChangedBy: string; + deletedAt: Date; + }[] + }> { + try { + const fileList = await this.prisma.file_upload.findMany({ + where: { + orgId: String(orgId), + OR: [ + { name: { contains: getAllfileDetails?.search, mode: 'insensitive' } }, + { status: { contains: getAllfileDetails?.search, mode: 'insensitive' } }, + { upload_type: { contains: getAllfileDetails?.search, mode: 'insensitive' } } + ] + }, + take: Number(getAllfileDetails?.pageSize), + skip: (getAllfileDetails?.pageNumber - 1) * getAllfileDetails?.pageSize + }); + const fileCount = await this.prisma.file_upload.count({ + where: { + orgId: String(orgId) + } + }); + return { fileCount, fileList }; + } catch (error) { + this.logger.error(`[getFileUploadDetails] - error: ${JSON.stringify(error)}`); + throw error; + } + } + + async getFileDetailsByFileId(fileId: unknown, getAllfileDetails: PreviewRequest): Promise<{ + fileCount: number + fileList: { + id: string; + referenceId: string; + isError: boolean; + error: string; + detailError: string; + createDateTime: Date; + createdBy: string; + lastChangedDateTime: Date; + lastChangedBy: string; + deletedAt: Date; + fileUploadId: string; + }[] + }> { + try { + const fileList = await this.prisma.file_data.findMany({ + where: { + fileUploadId: fileId, + OR: [ + { error: { contains: getAllfileDetails?.search, mode: 'insensitive' } }, + { referenceId: { contains: getAllfileDetails?.search, mode: 'insensitive' } }, + { detailError: { contains: getAllfileDetails?.search, mode: 'insensitive' } } + ] + }, + take: Number(getAllfileDetails?.pageSize), + skip: (getAllfileDetails?.pageNumber - 1) * getAllfileDetails?.pageSize + }); + const fileCount = await this.prisma.file_data.count({ + where: { + fileUploadId: fileId + } + }); + return { fileCount, fileList }; + } catch (error) { + this.logger.error(`[getFileDetailsByFileId] - error: ${JSON.stringify(error)}`); + throw error; + } + } + + async saveFileUploadData(fileUploadData: FileUploadData): Promise { try { const { fileUpload, isError, referenceId, error, detailError } = fileUploadData; diff --git a/apps/issuance/src/issuance.service.ts b/apps/issuance/src/issuance.service.ts index 9547301e1..93a183cc2 100644 --- a/apps/issuance/src/issuance.service.ts +++ b/apps/issuance/src/issuance.service.ts @@ -597,6 +597,65 @@ export class IssuanceService { } } + async getFileDetailsByFileId( + fileId: string, + getAllfileDetails: PreviewRequest + ): Promise { + try { + + const fileDetails = await this.issuanceRepository.getFileDetailsByFileId(fileId, getAllfileDetails); + + const fileResponse = { + totalItems: fileDetails.fileCount, + hasNextPage: getAllfileDetails.pageSize * getAllfileDetails.pageNumber < fileDetails.fileCount, + hasPreviousPage: 1 < getAllfileDetails.pageNumber, + nextPage: getAllfileDetails.pageNumber + 1, + previousPage: getAllfileDetails.pageNumber - 1, + lastPage: Math.ceil(fileDetails.fileCount / getAllfileDetails.pageSize), + data: fileDetails.fileList + }; + + if (0 !== fileDetails.fileCount) { + return fileResponse; + } else { + throw new NotFoundException(ResponseMessages.issuance.error.notFound); + } + + } catch (error) { + this.logger.error(`error in issuedFileDetails : ${error}`); + throw new RpcException(error.response); + } + } + + async issuedFileDetails( + orgId: string, + getAllfileDetails: PreviewRequest + ): Promise { + try { + + const fileDetails = await this.issuanceRepository.getAllFileDetails(orgId, getAllfileDetails); + + const fileResponse = { + totalItems: fileDetails.fileCount, + hasNextPage: getAllfileDetails.pageSize * getAllfileDetails.pageNumber < fileDetails.fileCount, + hasPreviousPage: 1 < getAllfileDetails.pageNumber, + nextPage: getAllfileDetails.pageNumber + 1, + previousPage: getAllfileDetails.pageNumber - 1, + lastPage: Math.ceil(fileDetails.fileCount / getAllfileDetails.pageSize), + data: fileDetails.fileList + }; + + if (0 !== fileDetails.fileCount) { + return fileResponse; + } else { + throw new NotFoundException(ResponseMessages.issuance.error.notFound); + } + + } catch (error) { + this.logger.error(`error in issuedFileDetails : ${error}`); + throw new RpcException(error.response); + } + } async issueBulkCredential(requestId: string, orgId: number): Promise { const fileUpload: { diff --git a/libs/common/src/response-messages/index.ts b/libs/common/src/response-messages/index.ts index 223ade83f..5c712659d 100644 --- a/libs/common/src/response-messages/index.ts +++ b/libs/common/src/response-messages/index.ts @@ -160,7 +160,8 @@ export const ResponseMessages = { fetch: 'Issue-credential fetched successfully', importCSV: 'File imported sucessfully', previewCSV: 'File details fetched sucessfully', - bulkIssuance: 'Bulk-issunace process started' + bulkIssuance: 'Bulk-issunace process started', + notFound: 'Schema records not found' }, error: { exists: 'Credentials is already exist', From 9dac8b43301a24ec75cc78a8663a6c87eca83dae Mon Sep 17 00:00:00 2001 From: Nishad Date: Wed, 8 Nov 2023 12:20:42 +0530 Subject: [PATCH 02/62] wip for certificate sharing feature Signed-off-by: Nishad --- .../organization/organization.controller.ts | 27 ++ .../src/organization/organization.service.ts | 12 + package.json | 1 + pnpm-lock.yaml | 395 ++++++++++++++++-- 4 files changed, 401 insertions(+), 34 deletions(-) diff --git a/apps/api-gateway/src/organization/organization.controller.ts b/apps/api-gateway/src/organization/organization.controller.ts index 3746ca933..ac4ceadc2 100644 --- a/apps/api-gateway/src/organization/organization.controller.ts +++ b/apps/api-gateway/src/organization/organization.controller.ts @@ -56,6 +56,33 @@ export class OrganizationController { return res.send(getImageBuffer); } + @Get('/certificate') + async convertHtmlToImage(@Res() res: Response): Promise { + + const htmlString = ` + + + + +
+
+ Header +
+
+ footer +
+
+ +`; + + const imageBuffer = await this.organizationService.convertHtmlToImage(htmlString); + + res.set('Content-Type', 'image/png'); + return res.status(HttpStatus.OK).send(imageBuffer); + + } + + /** * * @param user diff --git a/apps/api-gateway/src/organization/organization.service.ts b/apps/api-gateway/src/organization/organization.service.ts index 4dcfce609..34b1cadd8 100644 --- a/apps/api-gateway/src/organization/organization.service.ts +++ b/apps/api-gateway/src/organization/organization.service.ts @@ -9,6 +9,7 @@ import { BulkSendInvitationDto } from './dtos/send-invitation.dto'; import { UpdateUserRolesDto } from './dtos/update-user-roles.dto'; import { UpdateOrganizationDto } from './dtos/update-organization-dto'; import { GetAllUsersDto } from '../user/dto/get-all-users.dto'; +import * as puppeteer from 'puppeteer'; @Injectable() export class OrganizationService extends BaseService { @@ -142,4 +143,15 @@ export class OrganizationService extends BaseService { return this.sendNats(this.serviceProxy, 'fetch-organization-profile', payload); } + + async convertHtmlToImage(html: string): Promise { + const browser = await puppeteer.launch(); + const page = await browser.newPage(); + + await page.setContent(html); + const screenshot = await page.screenshot({path: 'cert1.png'}); + + await browser.close(); + return screenshot; + } } diff --git a/package.json b/package.json index 04c1e3dd2..e56c31173 100755 --- a/package.json +++ b/package.json @@ -84,6 +84,7 @@ "path": "^0.12.7", "pdfkit": "^0.13.0", "pg": "^8.11.2", + "puppeteer": "^21.5.0", "qrcode": "^1.5.3", "qs": "^6.11.2", "reflect-metadata": "^0.1.13", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 81ad79e2e..98dd534bd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -179,6 +179,9 @@ dependencies: pg: specifier: ^8.11.2 version: 8.11.2 + puppeteer: + specifier: ^21.5.0 + version: 21.5.0(typescript@5.1.6) qrcode: specifier: ^1.5.3 version: 1.5.3 @@ -406,7 +409,6 @@ packages: engines: {node: '>=6.9.0'} dependencies: '@babel/highlight': 7.22.5 - dev: true /@babel/compat-data@7.22.9: resolution: {integrity: sha512-5UamI7xkUcJ3i9qVDS+KFDEK8/7oJ55/sJMB1Ge7IEapr7KfdfV/HErR+koZwOfd+SgtFKOKRhRakdg++DcJpQ==} @@ -528,7 +530,6 @@ packages: /@babel/helper-validator-identifier@7.22.5: resolution: {integrity: sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==} engines: {node: '>=6.9.0'} - dev: true /@babel/helper-validator-option@7.22.5: resolution: {integrity: sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw==} @@ -553,7 +554,6 @@ packages: '@babel/helper-validator-identifier': 7.22.5 chalk: 2.4.2 js-tokens: 4.0.0 - dev: true /@babel/parser@7.22.7: resolution: {integrity: sha512-7NF8pOkHP5o2vpmGgNGcfAeCvOYhGLyA3Z4eBQkT1RJlWu47n63bCs93QfJ2hIAFCil7L5P2IWhs1oToVgrL0Q==} @@ -1615,6 +1615,22 @@ packages: resolution: {integrity: sha512-NV/4nVNWFZSJCCIA3HIFJbbDKO/NARc9ej0tX5S9k2EVbkrFJC4Xt9b0u4rNZWL4V+F5LAjvta8vzEUw0rw+HA==} requiresBuild: true + /@puppeteer/browsers@1.8.0: + resolution: {integrity: sha512-TkRHIV6k2D8OlUe8RtG+5jgOF/H98Myx0M6AOafC8DdNVOFiBSFa5cpRDtpm8LXOa9sVwe0+e6Q3FC56X/DZfg==} + engines: {node: '>=16.3.0'} + hasBin: true + dependencies: + debug: 4.3.4 + extract-zip: 2.0.1 + progress: 2.0.3 + proxy-agent: 6.3.1 + tar-fs: 3.0.4 + unbzip2-stream: 1.4.3 + yargs: 17.7.2 + transitivePeerDependencies: + - supports-color + dev: false + /@sendgrid/client@7.7.0: resolution: {integrity: sha512-SxH+y8jeAQSnDavrTD0uGDXYIIkFylCo+eDofVmZLQ0f862nnqbC3Vd1ej6b7Le7lboyzQF6F7Fodv02rYspuA==} engines: {node: 6.* || 8.* || >=10.*} @@ -1727,6 +1743,10 @@ packages: tslib: 2.6.1 dev: false + /@tootallnate/quickjs-emscripten@0.23.0: + resolution: {integrity: sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==} + dev: false + /@tsconfig/node10@1.0.9: resolution: {integrity: sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==} @@ -2007,6 +2027,14 @@ packages: '@types/yargs-parser': 21.0.0 dev: true + /@types/yauzl@2.10.2: + resolution: {integrity: sha512-Km7XAtUIduROw7QPgvcft0lIupeG8a8rdKL8RiSyKvlE7dYY31fEn41HVuQsRFDuROA8tA4K2UVL+WdfFmErBA==} + requiresBuild: true + dependencies: + '@types/node': 20.4.6 + dev: false + optional: true + /@typescript-eslint/eslint-plugin@6.2.1(@typescript-eslint/parser@6.2.1)(eslint@8.46.0)(typescript@5.1.6): resolution: {integrity: sha512-iZVM/ALid9kO0+I81pnp1xmYiFyqibAHzrqX4q5YvvVEyJqY+e6rfTXSCsc2jUxGNqJqTfFSSij/NFkZBiBzLw==} engines: {node: ^16.0.0 || >=18.0.0} @@ -2439,7 +2467,6 @@ packages: engines: {node: '>=4'} dependencies: color-convert: 1.9.3 - dev: true /ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} @@ -2600,6 +2627,13 @@ packages: dev: false optional: true + /ast-types@0.13.4: + resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==} + engines: {node: '>=4'} + dependencies: + tslib: 2.6.2 + dev: false + /astral-regex@2.0.0: resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} engines: {node: '>=8'} @@ -2663,6 +2697,10 @@ packages: - debug dev: false + /b4a@1.6.4: + resolution: {integrity: sha512-fpWrvyVHEKyeEvbKZTVOeZF3VSKKWtJxFIxX/jaVPf+cLbGUSitjb49pHLqPV2BUNNZ0LcoeEGfE/YCpyDYHIw==} + dev: false + /babel-jest@29.6.2(@babel/core@7.22.9): resolution: {integrity: sha512-BYCzImLos6J3BH/+HvUCHG1dTf2MzmAB4jaVxHV+29RZLjR29XuYTmsf2sdDwkrb+FczkGo3kOhE7ga6sI0P4A==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -2750,6 +2788,11 @@ packages: resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==} engines: {node: ^4.5.0 || >= 5.9} + /basic-ftp@5.0.3: + resolution: {integrity: sha512-QHX8HLlncOLpy54mh+k/sWIFd0ThmRqwe9ZjELybGZK+tZ8rUb9VO0saKJUROTbE+KhzDUT7xziGpGrW8Kmd+g==} + engines: {node: '>=10.0.0'} + dev: false + /bcrypt-pbkdf@1.0.2: resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==} requiresBuild: true @@ -2904,7 +2947,6 @@ packages: resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} requiresBuild: true dev: false - optional: true /buffer-equal-constant-time@1.0.1: resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} @@ -2928,7 +2970,6 @@ packages: dependencies: base64-js: 1.5.1 ieee754: 1.2.1 - dev: true /buffer@6.0.3: resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} @@ -3003,7 +3044,6 @@ packages: /callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} - dev: true /camelcase@5.3.1: resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} @@ -3037,7 +3077,6 @@ packages: ansi-styles: 3.2.1 escape-string-regexp: 1.0.5 supports-color: 5.5.0 - dev: true /chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} @@ -3085,6 +3124,16 @@ packages: engines: {node: '>=6.0'} dev: true + /chromium-bidi@0.4.33(devtools-protocol@0.0.1203626): + resolution: {integrity: sha512-IxoFM5WGQOIAd95qrSXzJUv4eXIrh+RvU3rwwqIiwYuvfE7U/Llj4fejbsJnjJMUYCuGtVQsY2gv7oGl4aTNSQ==} + peerDependencies: + devtools-protocol: '*' + dependencies: + devtools-protocol: 0.0.1203626 + mitt: 3.0.1 + urlpattern-polyfill: 9.0.0 + dev: false + /ci-info@3.8.0: resolution: {integrity: sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==} engines: {node: '>=8'} @@ -3216,7 +3265,6 @@ packages: resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} dependencies: color-name: 1.1.3 - dev: true /color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} @@ -3226,7 +3274,6 @@ packages: /color-name@1.1.3: resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} - dev: true /color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} @@ -3357,6 +3404,22 @@ packages: yaml: 1.10.2 dev: true + /cosmiconfig@8.3.6(typescript@5.1.6): + resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + import-fresh: 3.3.0 + js-yaml: 4.1.0 + parse-json: 5.2.0 + path-type: 4.0.0 + typescript: 5.1.6 + dev: false + /create-require@1.1.1: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} @@ -3381,6 +3444,14 @@ packages: - encoding dev: false + /cross-fetch@4.0.0: + resolution: {integrity: sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==} + dependencies: + node-fetch: 2.6.12 + transitivePeerDependencies: + - encoding + dev: false + /cross-spawn@7.0.3: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} @@ -3417,6 +3488,11 @@ packages: dev: false optional: true + /data-uri-to-buffer@6.0.1: + resolution: {integrity: sha512-MZd3VlchQkp8rdend6vrx7MmVDJzSNTBvghvKjirLkD+WTChA3KUf0jkE68Q4UyctNqI11zZO9/x2Yx+ub5Cvg==} + engines: {node: '>= 14'} + dev: false + /date-fns@2.30.0: resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} engines: {node: '>=0.11'} @@ -3514,6 +3590,15 @@ packages: has-property-descriptors: 1.0.0 object-keys: 1.1.1 + /degenerator@5.0.1: + resolution: {integrity: sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==} + engines: {node: '>= 14'} + dependencies: + ast-types: 0.13.4 + escodegen: 2.1.0 + esprima: 4.0.1 + dev: false + /delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} @@ -3551,6 +3636,10 @@ packages: engines: {node: '>=8'} dev: true + /devtools-protocol@0.0.1203626: + resolution: {integrity: sha512-nEzHZteIUZfGCZtTiS1fRpC8UZmsfD1SiyPvaUNvS13dvKf666OAm8YTi0+Ca3n1nLEyu49Cy4+dPWpaHFJk9g==} + dev: false + /dezalgo@1.0.4: resolution: {integrity: sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==} dependencies: @@ -3666,7 +3755,6 @@ packages: resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} dependencies: once: 1.4.0 - dev: true /engine.io-client@6.5.2: resolution: {integrity: sha512-CQZqbrpEYnrpGqC07a9dJDz4gePZUgTPMU3NKJPSeQOyw27Tst4Pl3FemKoFGAlHzgZmKjoRmiJvbWfhCXUlIg==} @@ -3717,7 +3805,6 @@ packages: resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} dependencies: is-arrayish: 0.2.1 - dev: true /es-abstract@1.22.1: resolution: {integrity: sha512-ioRRcXMO6OFyRpyzV3kE1IIBd4WG5/kltnzdxSCqoP8CMGs/Li+M1uF5o7lOkZVFjDs+NLesthnF66Pg/0q0Lw==} @@ -3845,7 +3932,6 @@ packages: /escape-string-regexp@1.0.5: resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} engines: {node: '>=0.8.0'} - dev: true /escape-string-regexp@2.0.0: resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} @@ -3857,6 +3943,18 @@ packages: engines: {node: '>=10'} dev: true + /escodegen@2.1.0: + resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} + engines: {node: '>=6.0'} + hasBin: true + dependencies: + esprima: 4.0.1 + estraverse: 5.3.0 + esutils: 2.0.3 + optionalDependencies: + source-map: 0.6.1 + dev: false + /eslint-config-prettier@8.10.0(eslint@8.46.0): resolution: {integrity: sha512-SM8AMJdeQqRYT9O9zguiruQZaN7+z+E4eAP9oiLNGKMtomwaB1E9dcgUD6ZAn/eQAb52USbvezbiljfZUhbJcg==} hasBin: true @@ -4139,7 +4237,6 @@ packages: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} engines: {node: '>=4'} hasBin: true - dev: true /esquery@1.5.0: resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==} @@ -4163,12 +4260,10 @@ packages: /estraverse@5.3.0: resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} engines: {node: '>=4.0'} - dev: true /esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} - dev: true /etag@1.8.1: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} @@ -4314,6 +4409,20 @@ packages: dev: false optional: true + /extract-zip@2.0.1: + resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} + engines: {node: '>= 10.17.0'} + hasBin: true + dependencies: + debug: 4.3.4 + get-stream: 5.2.0 + yauzl: 2.10.0 + optionalDependencies: + '@types/yauzl': 2.10.2 + transitivePeerDependencies: + - supports-color + dev: false + /extsprintf@1.3.0: resolution: {integrity: sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==} engines: {'0': node >=0.6.0} @@ -4328,6 +4437,10 @@ packages: resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} dev: true + /fast-fifo@1.3.2: + resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} + dev: false + /fast-glob@3.3.1: resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==} engines: {node: '>=8.6.0'} @@ -4367,7 +4480,6 @@ packages: dependencies: pend: 1.2.0 dev: false - optional: true /figures@3.2.0: resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} @@ -4543,6 +4655,15 @@ packages: universalify: 2.0.0 dev: true + /fs-extra@8.1.0: + resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} + engines: {node: '>=6 <7 || >=8'} + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + dev: false + /fs-minipass@2.1.0: resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} engines: {node: '>= 8'} @@ -4646,7 +4767,6 @@ packages: engines: {node: '>=8'} dependencies: pump: 3.0.0 - dev: true /get-stream@6.0.1: resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} @@ -4661,6 +4781,18 @@ packages: get-intrinsic: 1.2.1 dev: true + /get-uri@6.0.2: + resolution: {integrity: sha512-5KLucCJobh8vBY1K07EFV4+cPZH3mrV9YeAruUseCQKHB58SGjjT2l9/eA9LD082IiuMjSlFJEcdJ27TXvbZNw==} + engines: {node: '>= 14'} + dependencies: + basic-ftp: 5.0.3 + data-uri-to-buffer: 6.0.1 + debug: 4.3.4 + fs-extra: 8.1.0 + transitivePeerDependencies: + - supports-color + dev: false + /getpass@0.1.7: resolution: {integrity: sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==} requiresBuild: true @@ -4774,7 +4906,6 @@ packages: /has-flag@3.0.0: resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} engines: {node: '>=4'} - dev: true /has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} @@ -4857,6 +4988,16 @@ packages: statuses: 2.0.1 toidentifier: 1.0.1 + /http-proxy-agent@7.0.0: + resolution: {integrity: sha512-+ZT+iBxVUQ1asugqnD6oWoRiS25AkjNfG085dKJGtGxkdwLQrMKU5wJr2bOOFAXzKcTuqq+7fZlTMgG3SRfIYQ==} + engines: {node: '>= 14'} + dependencies: + agent-base: 7.1.0 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: false + /http-signature@1.2.0: resolution: {integrity: sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==} engines: {node: '>=0.8', npm: '>=1.3.7'} @@ -4895,6 +5036,16 @@ packages: - supports-color dev: false + /https-proxy-agent@7.0.2: + resolution: {integrity: sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA==} + engines: {node: '>= 14'} + dependencies: + agent-base: 7.1.0 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: false + /human-signals@1.1.1: resolution: {integrity: sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==} engines: {node: '>=8.12.0'} @@ -4947,7 +5098,6 @@ packages: dependencies: parent-module: 1.0.1 resolve-from: 4.0.0 - dev: true /import-local@3.1.0: resolution: {integrity: sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==} @@ -5053,6 +5203,14 @@ packages: - supports-color dev: false + /ip@1.1.8: + resolution: {integrity: sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg==} + dev: false + + /ip@2.0.0: + resolution: {integrity: sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==} + dev: false + /ipaddr.js@1.9.1: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} @@ -5074,7 +5232,6 @@ packages: /is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} - dev: true /is-bigint@1.0.4: resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==} @@ -5740,7 +5897,6 @@ packages: /js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - dev: true /js-yaml@3.14.1: resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} @@ -5774,7 +5930,6 @@ packages: /json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} - dev: true /json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} @@ -5835,6 +5990,12 @@ packages: dev: false optional: true + /jsonfile@4.0.0: + resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} + optionalDependencies: + graceful-fs: 4.2.11 + dev: false + /jsonfile@6.1.0: resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} dependencies: @@ -5981,7 +6142,6 @@ packages: /lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - dev: true /lint-staged@13.2.3: resolution: {integrity: sha512-zVVEXLuQIhr1Y7R7YAWx4TZLdvuzk7DnmrsTNL0fax6Z3jrpFcas+vKbzxhhvp6TA55m1SQuWkpzI1qbfDZbAg==} @@ -6119,6 +6279,11 @@ packages: dependencies: yallist: 4.0.0 + /lru-cache@7.18.3: + resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} + engines: {node: '>=12'} + dev: false + /lru-memoizer@2.2.0: resolution: {integrity: sha512-QfOZ6jNkxCcM/BkIPnFsqDhtrazLRsghi9mBwFAzol5GCvj4EkFT899Za3+QwikCg5sRX8JstioBDwOxEyzaNw==} dependencies: @@ -6281,6 +6446,14 @@ packages: yallist: 4.0.0 dev: false + /mitt@3.0.1: + resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + dev: false + + /mkdirp-classic@0.5.3: + resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + dev: false + /mkdirp@0.5.6: resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} hasBin: true @@ -6407,6 +6580,11 @@ packages: typeorm: 0.3.10(pg@8.11.2)(ts-node@10.9.1) dev: false + /netmask@2.0.2: + resolution: {integrity: sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==} + engines: {node: '>= 0.4.0'} + dev: false + /next-tick@1.1.0: resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==} dev: false @@ -6670,6 +6848,31 @@ packages: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} + /pac-proxy-agent@7.0.1: + resolution: {integrity: sha512-ASV8yU4LLKBAjqIPMbrgtaKIvxQri/yh2OpI+S6hVa9JRkUI3Y3NPFbfngDtY7oFtSMD3w31Xns89mDa3Feo5A==} + engines: {node: '>= 14'} + dependencies: + '@tootallnate/quickjs-emscripten': 0.23.0 + agent-base: 7.1.0 + debug: 4.3.4 + get-uri: 6.0.2 + http-proxy-agent: 7.0.0 + https-proxy-agent: 7.0.2 + pac-resolver: 7.0.0 + socks-proxy-agent: 8.0.2 + transitivePeerDependencies: + - supports-color + dev: false + + /pac-resolver@7.0.0: + resolution: {integrity: sha512-Fd9lT9vJbHYRACT8OhCbZBbxr6KRSawSovFpy8nDGshaK99S/EBhVIHp9+crhxrsZOuvLpgL1n23iyPg6Rl2hg==} + engines: {node: '>= 14'} + dependencies: + degenerator: 5.0.1 + ip: 1.1.8 + netmask: 2.0.2 + dev: false + /packet-reader@1.0.0: resolution: {integrity: sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==} dev: false @@ -6687,7 +6890,6 @@ packages: engines: {node: '>=6'} dependencies: callsites: 3.1.0 - dev: true /parse-json@5.2.0: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} @@ -6697,7 +6899,6 @@ packages: error-ex: 1.3.2 json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 - dev: true /parse5-htmlparser2-tree-adapter@6.0.1: resolution: {integrity: sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==} @@ -6784,7 +6985,6 @@ packages: /path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} - dev: true /path@0.12.7: resolution: {integrity: sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q==} @@ -6810,7 +7010,6 @@ packages: resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} requiresBuild: true dev: false - optional: true /performance-now@2.1.0: resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==} @@ -7031,6 +7230,11 @@ packages: dev: false optional: true + /progress@2.0.3: + resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} + engines: {node: '>=0.4.0'} + dev: false + /prompts@2.4.2: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} @@ -7046,6 +7250,22 @@ packages: forwarded: 0.2.0 ipaddr.js: 1.9.1 + /proxy-agent@6.3.1: + resolution: {integrity: sha512-Rb5RVBy1iyqOtNl15Cw/llpeLH8bsb37gM1FUfKQ+Wck6xHlbAhWGUFiTRHtkjqGTA5pSHz6+0hrPW/oECihPQ==} + engines: {node: '>= 14'} + dependencies: + agent-base: 7.1.0 + debug: 4.3.4 + http-proxy-agent: 7.0.0 + https-proxy-agent: 7.0.2 + lru-cache: 7.18.3 + pac-proxy-agent: 7.0.1 + proxy-from-env: 1.1.0 + socks-proxy-agent: 8.0.2 + transitivePeerDependencies: + - supports-color + dev: false + /proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} dev: false @@ -7065,12 +7285,44 @@ packages: dependencies: end-of-stream: 1.4.4 once: 1.4.0 - dev: true /punycode@2.3.0: resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==} engines: {node: '>=6'} + /puppeteer-core@21.5.0: + resolution: {integrity: sha512-qG0RJ6qKgFz09UUZxDB9IcyTJGypQXMuE8WmEoHk7kgjutmRiOVv5RgsyUkY67AxDdBWx21bn1PHHRJnO/6b4A==} + engines: {node: '>=16.3.0'} + dependencies: + '@puppeteer/browsers': 1.8.0 + chromium-bidi: 0.4.33(devtools-protocol@0.0.1203626) + cross-fetch: 4.0.0 + debug: 4.3.4 + devtools-protocol: 0.0.1203626 + ws: 8.14.2 + transitivePeerDependencies: + - bufferutil + - encoding + - supports-color + - utf-8-validate + dev: false + + /puppeteer@21.5.0(typescript@5.1.6): + resolution: {integrity: sha512-prvy9rdauyIaaEgefQRcw9zhQnYQbl8O1Gj5VJazKJ7kwNx703+Paw/1bwA+b96jj/S+r55hrmF5SfiEG5PUcg==} + engines: {node: '>=16.3.0'} + requiresBuild: true + dependencies: + '@puppeteer/browsers': 1.8.0 + cosmiconfig: 8.3.6(typescript@5.1.6) + puppeteer-core: 21.5.0 + transitivePeerDependencies: + - bufferutil + - encoding + - supports-color + - typescript + - utf-8-validate + dev: false + /pure-rand@6.0.2: resolution: {integrity: sha512-6Yg0ekpKICSjPswYOuC5sku/TSWaRYlA0qsXqJgM/d/4pLPHPuTxK7Nbf7jFKzAeedUhR8C7K9Uv63FBsSo8xQ==} dev: true @@ -7109,6 +7361,10 @@ packages: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} dev: true + /queue-tick@1.0.1: + resolution: {integrity: sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==} + dev: false + /randombytes@2.1.0: resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} dependencies: @@ -7285,7 +7541,6 @@ packages: /resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} - dev: true /resolve-from@5.0.0: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} @@ -7556,6 +7811,11 @@ packages: is-fullwidth-code-point: 4.0.0 dev: true + /smart-buffer@4.2.0: + resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} + engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} + dev: false + /socket.io-adapter@2.5.2: resolution: {integrity: sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA==} dependencies: @@ -7603,6 +7863,25 @@ packages: - supports-color - utf-8-validate + /socks-proxy-agent@8.0.2: + resolution: {integrity: sha512-8zuqoLv1aP/66PHF5TqwJ7Czm3Yv32urJQHrVyhD7mmA6d61Zv8cIXQYPTWwmg6qlupnPvs/QKDmfa4P/qct2g==} + engines: {node: '>= 14'} + dependencies: + agent-base: 7.1.0 + debug: 4.3.4 + socks: 2.7.1 + transitivePeerDependencies: + - supports-color + dev: false + + /socks@2.7.1: + resolution: {integrity: sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==} + engines: {node: '>= 10.13.0', npm: '>= 3.0.0'} + dependencies: + ip: 2.0.0 + smart-buffer: 4.2.0 + dev: false + /source-map-support@0.5.13: resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==} dependencies: @@ -7620,7 +7899,6 @@ packages: /source-map@0.6.1: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} - dev: true /source-map@0.7.4: resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} @@ -7680,6 +7958,13 @@ packages: resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} engines: {node: '>=10.0.0'} + /streamx@2.15.2: + resolution: {integrity: sha512-b62pAV/aeMjUoRN2C/9F0n+G8AfcJjNC0zw/ZmOHeFsIe4m4GzjVW9m6VHXVjk536NbdU9JRwKMJRfkc+zUFTg==} + dependencies: + fast-fifo: 1.3.2 + queue-tick: 1.0.1 + dev: false + /string-argv@0.3.2: resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} engines: {node: '>=0.6.19'} @@ -7835,7 +8120,6 @@ packages: engines: {node: '>=4'} dependencies: has-flag: 3.0.0 - dev: true /supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} @@ -7883,6 +8167,22 @@ packages: engines: {node: '>=6'} dev: true + /tar-fs@3.0.4: + resolution: {integrity: sha512-5AFQU8b9qLfZCX9zp2duONhPmZv0hGYiBPJsyUdqMjzq/mqVpy/rEUSeHk1+YitmxugaptgBh5oDGU3VsAJq4w==} + dependencies: + mkdirp-classic: 0.5.3 + pump: 3.0.0 + tar-stream: 3.1.6 + dev: false + + /tar-stream@3.1.6: + resolution: {integrity: sha512-B/UyjYwPpMBv+PaFSWAmtYjwdrlEaZQEhMIBFNC5oEG8lpiW8XjcSdmEaClj28ArfKScKHs2nshz3k2le6crsg==} + dependencies: + b4a: 1.6.4 + fast-fifo: 1.3.2 + streamx: 2.15.2 + dev: false + /tar@6.1.15: resolution: {integrity: sha512-/zKt9UyngnxIT/EAGYuxaMYgOIJiP81ab9ZfkILq4oNLPFX50qyYmu7jRj9qeXoxmJHjGlbH0+cm2uy1WCs10A==} engines: {node: '>=10'} @@ -7988,7 +8288,6 @@ packages: /through@2.3.8: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} - dev: true /tiny-inflate@1.0.3: resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==} @@ -8389,6 +8688,13 @@ packages: which-boxed-primitive: 1.0.2 dev: true + /unbzip2-stream@1.4.3: + resolution: {integrity: sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==} + dependencies: + buffer: 5.7.1 + through: 2.3.8 + dev: false + /unfetch@4.2.0: resolution: {integrity: sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA==} dev: false @@ -8407,6 +8713,11 @@ packages: tiny-inflate: 1.0.3 dev: false + /universalify@0.1.2: + resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} + engines: {node: '>= 4.0.0'} + dev: false + /universalify@2.0.0: resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==} engines: {node: '>= 10.0.0'} @@ -8451,6 +8762,10 @@ packages: resolution: {integrity: sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==} dev: false + /urlpattern-polyfill@9.0.0: + resolution: {integrity: sha512-WHN8KDQblxd32odxeIgo83rdVDE2bvdkb86it7bMhYZwWKJz0+O0RK/eZiHYnM+zgt/U7hAHOlCQGfjjvSkw2g==} + dev: false + /urlsafe-base64@1.0.0: resolution: {integrity: sha512-RtuPeMy7c1UrHwproMZN9gN6kiZ0SvJwRaEzwZY0j9MypEkFqyBaKv176jvlPtg58Zh36bOkS0NFABXMHvvGCA==} dev: false @@ -8777,6 +9092,19 @@ packages: utf-8-validate: optional: true + /ws@8.14.2: + resolution: {integrity: sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + dev: false + /xml-js@1.6.11: resolution: {integrity: sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==} hasBin: true @@ -8906,7 +9234,6 @@ packages: buffer-crc32: 0.2.13 fd-slicer: 1.1.0 dev: false - optional: true /yn@3.1.1: resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} From 01898419ce9692d959d6dfa85b0f4ef96774d310 Mon Sep 17 00:00:00 2001 From: tipusinghaw Date: Wed, 8 Nov 2023 15:31:37 +0530 Subject: [PATCH 03/62] refactor: changed the variable name Signed-off-by: tipusinghaw --- apps/issuance/src/issuance.repository.ts | 6 +++--- apps/issuance/src/issuance.service.ts | 13 ++++++------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/apps/issuance/src/issuance.repository.ts b/apps/issuance/src/issuance.repository.ts index 2880f26d0..cf8da558c 100644 --- a/apps/issuance/src/issuance.repository.ts +++ b/apps/issuance/src/issuance.repository.ts @@ -279,7 +279,7 @@ export class IssuanceRepository { async getFileDetailsByFileId(fileId: unknown, getAllfileDetails: PreviewRequest): Promise<{ fileCount: number - fileList: { + fileDataList: { id: string; referenceId: string; isError: boolean; @@ -294,7 +294,7 @@ export class IssuanceRepository { }[] }> { try { - const fileList = await this.prisma.file_data.findMany({ + const fileDataList = await this.prisma.file_data.findMany({ where: { fileUploadId: fileId, OR: [ @@ -311,7 +311,7 @@ export class IssuanceRepository { fileUploadId: fileId } }); - return { fileCount, fileList }; + return { fileCount, fileDataList }; } catch (error) { this.logger.error(`[getFileDetailsByFileId] - error: ${JSON.stringify(error)}`); throw error; diff --git a/apps/issuance/src/issuance.service.ts b/apps/issuance/src/issuance.service.ts index 73b0e098f..dbc96a707 100644 --- a/apps/issuance/src/issuance.service.ts +++ b/apps/issuance/src/issuance.service.ts @@ -606,19 +606,19 @@ export class IssuanceService { ): Promise { try { - const fileDetails = await this.issuanceRepository.getFileDetailsByFileId(fileId, getAllfileDetails); + const fileData = await this.issuanceRepository.getFileDetailsByFileId(fileId, getAllfileDetails); const fileResponse = { - totalItems: fileDetails.fileCount, - hasNextPage: getAllfileDetails.pageSize * getAllfileDetails.pageNumber < fileDetails.fileCount, + totalItems: fileData.fileCount, + hasNextPage: getAllfileDetails.pageSize * getAllfileDetails.pageNumber < fileData.fileCount, hasPreviousPage: 1 < getAllfileDetails.pageNumber, nextPage: getAllfileDetails.pageNumber + 1, previousPage: getAllfileDetails.pageNumber - 1, - lastPage: Math.ceil(fileDetails.fileCount / getAllfileDetails.pageSize), - data: fileDetails.fileList + lastPage: Math.ceil(fileData.fileCount / getAllfileDetails.pageSize), + data: fileData.fileDataList }; - if (0 !== fileDetails.fileCount) { + if (0 !== fileData.fileCount) { return fileResponse; } else { throw new NotFoundException(ResponseMessages.issuance.error.notFound); @@ -637,7 +637,6 @@ export class IssuanceService { try { const fileDetails = await this.issuanceRepository.getAllFileDetails(orgId, getAllfileDetails); - const fileResponse = { totalItems: fileDetails.fileCount, hasNextPage: getAllfileDetails.pageSize * getAllfileDetails.pageNumber < fileDetails.fileCount, From 4be3e5162f56b0b5f6b13b93ccbd9bf954b9f6a4 Mon Sep 17 00:00:00 2001 From: pallavicoder Date: Wed, 8 Nov 2023 18:51:22 +0530 Subject: [PATCH 04/62] refact:sort org list by asc order Signed-off-by: pallavicoder --- apps/organization/repositories/organization.repository.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/organization/repositories/organization.repository.ts b/apps/organization/repositories/organization.repository.ts index 0ec7ca182..4d0d6f968 100644 --- a/apps/organization/repositories/organization.repository.ts +++ b/apps/organization/repositories/organization.repository.ts @@ -375,6 +375,7 @@ export class OrganizationRepository { pageSize: number ): Promise { try { + const sortByName = 'asc'; const result = await this.prisma.$transaction([ this.prisma.organisation.findMany({ where: { @@ -394,7 +395,8 @@ export class OrganizationRepository { take: pageSize, skip: (pageNumber - 1) * pageSize, orderBy: { - createDateTime: 'desc' + name: sortByName + } }), this.prisma.organisation.count({ From e054d8181928bf7c2ccff2ab1c112dd0ada9563e Mon Sep 17 00:00:00 2001 From: bhavanakarwade Date: Wed, 8 Nov 2023 18:58:13 +0530 Subject: [PATCH 05/62] feat: share user certificate Signed-off-by: bhavanakarwade --- .../organization/organization.controller.ts | 7 +---- .../src/user/dto/share-certificate.dto.ts | 31 +++++++++++++++++++ apps/api-gateway/src/user/user.controller.ts | 19 ++++++++++++ apps/api-gateway/src/user/user.service.ts | 8 +++++ apps/user/interfaces/user.interface.ts | 5 +++ apps/user/src/user.controller.ts | 14 ++++++++- apps/user/src/user.service.ts | 30 +++++++++++++++++- apps/user/templates/arbiter-template.ts | 28 +++++++++++++++++ apps/user/templates/participant-template.ts | 27 ++++++++++++++++ apps/user/templates/winner-template.ts | 25 +++++++++++++++ apps/user/templates/world-record-template.ts | 28 +++++++++++++++++ libs/enum/src/enum.ts | 7 +++++ 12 files changed, 221 insertions(+), 8 deletions(-) create mode 100644 apps/api-gateway/src/user/dto/share-certificate.dto.ts create mode 100644 apps/user/templates/arbiter-template.ts create mode 100644 apps/user/templates/participant-template.ts create mode 100644 apps/user/templates/winner-template.ts create mode 100644 apps/user/templates/world-record-template.ts diff --git a/apps/api-gateway/src/organization/organization.controller.ts b/apps/api-gateway/src/organization/organization.controller.ts index ac4ceadc2..f2b17e0a2 100644 --- a/apps/api-gateway/src/organization/organization.controller.ts +++ b/apps/api-gateway/src/organization/organization.controller.ts @@ -1,13 +1,9 @@ import { ApiBearerAuth, ApiForbiddenResponse, ApiOperation, ApiParam, ApiQuery, ApiResponse, ApiTags, ApiUnauthorizedResponse } from '@nestjs/swagger'; import { CommonService } from '@credebl/common'; -import { Controller, Get, Put, Param, UseGuards, UseFilters } from '@nestjs/common'; +import { Controller, Get, Put, Param, UseGuards, UseFilters, Post, Body, Res, HttpStatus, Query } from '@nestjs/common'; import { OrganizationService } from './organization.service'; -import { Post } from '@nestjs/common'; -import { Body } from '@nestjs/common'; -import { Res } from '@nestjs/common'; import { CreateOrganizationDto } from './dtos/create-organization-dto'; import IResponseType from '@credebl/common/interfaces/response.interface'; -import { HttpStatus } from '@nestjs/common'; import { Response } from 'express'; import { ApiResponseDto } from '../dtos/apiResponse.dto'; import { UnauthorizedErrorDto } from '../dtos/unauthorized-error.dto'; @@ -21,7 +17,6 @@ import { OrgRolesGuard } from '../authz/guards/org-roles.guard'; import { Roles } from '../authz/decorators/roles.decorator'; import { OrgRoles } from 'libs/org-roles/enums'; import { UpdateUserRolesDto } from './dtos/update-user-roles.dto'; -import { Query } from '@nestjs/common'; import { GetAllOrganizationsDto } from './dtos/get-all-organizations.dto'; import { GetAllSentInvitationsDto } from './dtos/get-all-sent-invitations.dto'; import { UpdateOrganizationDto } from './dtos/update-organization-dto'; diff --git a/apps/api-gateway/src/user/dto/share-certificate.dto.ts b/apps/api-gateway/src/user/dto/share-certificate.dto.ts new file mode 100644 index 000000000..5e03522f8 --- /dev/null +++ b/apps/api-gateway/src/user/dto/share-certificate.dto.ts @@ -0,0 +1,31 @@ +import { IsArray, IsNotEmpty, IsString } from 'class-validator'; + +import { ApiProperty } from '@nestjs/swagger'; + +class AttributeValue { + + @IsString() + @IsNotEmpty({ message: 'name is required.' }) + name: string; + + @IsString() + @IsNotEmpty({ message: 'winner' }) + userType: string; +} + +export class CreateUserCertificateDto { + + schemaId: string; + + @ApiProperty({ + 'example': [ + { + name: 'John Doe', + userType: 'winner' + } + ] + }) + @IsArray({ message: 'attributes must be an array' }) + @IsNotEmpty({ message: 'please provide valid attributes' }) + attributes: AttributeValue[]; +} diff --git a/apps/api-gateway/src/user/user.controller.ts b/apps/api-gateway/src/user/user.controller.ts index df5476a9b..f775df913 100644 --- a/apps/api-gateway/src/user/user.controller.ts +++ b/apps/api-gateway/src/user/user.controller.ts @@ -33,6 +33,7 @@ import { UpdatePlatformSettingsDto } from './dto/update-platform-settings.dto'; import { Roles } from '../authz/decorators/roles.decorator'; import { OrgRolesGuard } from '../authz/guards/org-roles.guard'; import { OrgRoles } from 'libs/org-roles/enums'; +import { CreateUserCertificateDto } from './dto/share-certificate.dto'; @UseFilters(CustomExceptionFilter) @Controller('users') @@ -256,6 +257,24 @@ export class UserController { } + @Post('/certificate') + @ApiOperation({ + summary: 'Share user certificate', + description: 'Share user certificate' + }) + @ApiResponse({ status: 200, description: 'Success', type: ApiResponseDto }) + @UseGuards(AuthGuard('jwt')) + @ApiBearerAuth() + async shareUserCertificate (@Body() shareUserCredentials: CreateUserCertificateDto, @Res() res: Response): Promise { + const userCertificateDetails = await this.userService.shareUserCertificate(shareUserCredentials); + const finalResponse: IResponseType = { + statusCode: HttpStatus.CREATED, + message: 'Certificate created successfully', + data: userCertificateDetails.response + }; + return res.status(HttpStatus.CREATED).json(finalResponse); + } + @Put('/') @ApiOperation({ summary: 'Update user profile', diff --git a/apps/api-gateway/src/user/user.service.ts b/apps/api-gateway/src/user/user.service.ts index 4409f0f8f..5138cd8e7 100644 --- a/apps/api-gateway/src/user/user.service.ts +++ b/apps/api-gateway/src/user/user.service.ts @@ -8,6 +8,7 @@ import { GetAllUsersDto } from './dto/get-all-users.dto'; import { UpdateUserProfileDto } from './dto/update-user-profile.dto'; import { AddPasskeyDetails } from './dto/add-user.dto'; import { UpdatePlatformSettingsDto } from './dto/update-platform-settings.dto'; +import { CreateUserCertificateDto } from './dto/share-certificate.dto'; @Injectable() export class UserService extends BaseService { @@ -50,6 +51,13 @@ export class UserService extends BaseService { return this.sendNats(this.serviceProxy, 'accept-reject-invitations', payload); } + async shareUserCertificate( + shareUserCredentials: CreateUserCertificateDto + ): Promise<{ response: object }> { + const payload = { shareUserCredentials}; + return this.sendNats(this.serviceProxy, 'share-user-certificate', payload); + } + async get( getAllUsersDto: GetAllUsersDto ): Promise<{ response: object }> { diff --git a/apps/user/interfaces/user.interface.ts b/apps/user/interfaces/user.interface.ts index 7ad363215..29dc6ff17 100644 --- a/apps/user/interfaces/user.interface.ts +++ b/apps/user/interfaces/user.interface.ts @@ -62,3 +62,8 @@ export interface PlatformSettingsI { enableEcosystem: boolean; multiEcosystemSupport: boolean; } + +export interface ShareUserCertificateI { + schemaId: string; + attributes: string[] +} diff --git a/apps/user/src/user.controller.ts b/apps/user/src/user.controller.ts index 5d3c3b3bb..2e8078f7b 100644 --- a/apps/user/src/user.controller.ts +++ b/apps/user/src/user.controller.ts @@ -1,4 +1,4 @@ -import { AddPasskeyDetails, PlatformSettingsI, UpdateUserProfile, UserEmailVerificationDto, UserI, userInfo } from '../interfaces/user.interface'; +import { AddPasskeyDetails, PlatformSettingsI, ShareUserCertificateI, UpdateUserProfile, UserEmailVerificationDto, UserI, userInfo } from '../interfaces/user.interface'; import { AcceptRejectInvitationDto } from '../dtos/accept-reject-invitation.dto'; import { Controller } from '@nestjs/common'; @@ -79,6 +79,18 @@ export class UserController { return this.userService.acceptRejectInvitations(payload.acceptRejectInvitation, payload.userId); } + /** + * + * @param payload + * @returns Share user certificate + */ + @MessagePattern({ cmd: 'share-user-certificate' }) + async shareUserCertificate(payload: { + shareUserCertificate: ShareUserCertificateI; + }): Promise { + return this.userService.shareUserCertificate(payload.shareUserCertificate); + } + /** * * @param payload diff --git a/apps/user/src/user.service.ts b/apps/user/src/user.service.ts index a8e25762f..353be69b9 100644 --- a/apps/user/src/user.service.ts +++ b/apps/user/src/user.service.ts @@ -29,6 +29,7 @@ import { AddPasskeyDetails, InvitationsI, PlatformSettingsI, + ShareUserCertificateI, UpdateUserProfile, UserEmailVerificationDto, UserI, @@ -39,7 +40,10 @@ import { UserActivityService } from '@credebl/user-activity'; import { SupabaseService } from '@credebl/supabase'; import { UserDevicesRepository } from '../repositories/user-device.repository'; import { v4 as uuidv4 } from 'uuid'; -import { EcosystemConfigSettings } from '@credebl/enum/enum'; +import { EcosystemConfigSettings, UserCertificateId } from '@credebl/enum/enum'; +import { WinnerTemplate } from '../templates/winner-template'; +import { ParticipantTemplate } from '../templates/participant-template'; +import { ArbiterTemplate } from '../templates/arbiter-template'; @Injectable() export class UserService { @@ -505,6 +509,30 @@ export class UserService { } } + /** + * + * @returns + */ + async shareUserCertificate(shareUserCertificate: ShareUserCertificateI): Promise { + const userWinnerTemplate = new WinnerTemplate(); + const userParticipantTemplate = new ParticipantTemplate(); + const userArbiterTemplate = new ArbiterTemplate(); + + const getWinnerTemplate = await userWinnerTemplate.getWinnerTemplate(); + const getParticipantTemplate = await userParticipantTemplate.getParticipantTemplate(); + const getArbiterTemplate = await userArbiterTemplate.getArbiterTemplate(); + + if (shareUserCertificate.schemaId === UserCertificateId.WINNER) { + return getWinnerTemplate; + } else if (shareUserCertificate.schemaId === UserCertificateId.PARTICIPANT) { + return getParticipantTemplate; + } else if (shareUserCertificate.schemaId === UserCertificateId.ARBITER) { + return getArbiterTemplate; + } else { + throw new NotFoundException(ResponseMessages.schema.error.invalidSchemaId); + } + } + /** * * @param acceptRejectInvitation diff --git a/apps/user/templates/arbiter-template.ts b/apps/user/templates/arbiter-template.ts new file mode 100644 index 000000000..f13fab108 --- /dev/null +++ b/apps/user/templates/arbiter-template.ts @@ -0,0 +1,28 @@ +export class ArbiterTemplate { + + public getArbiterTemplate(): string { + + try { + return ` + + + + + + Winner Template + + +
+
+ 🏆 +
+

Congratulations!

+

You're the Winner of our contest.

+
+ + `; + + } catch (error) { + } + } + } \ No newline at end of file diff --git a/apps/user/templates/participant-template.ts b/apps/user/templates/participant-template.ts new file mode 100644 index 000000000..15d3ab074 --- /dev/null +++ b/apps/user/templates/participant-template.ts @@ -0,0 +1,27 @@ +export class ParticipantTemplate { + + public getParticipantTemplate(): string { + + try { + return ` + + + + + + Participant Template + + +
+

John Doe

+

Participant ID: 12345

+

Email: john@example.com

+ +
+ + `; + + } catch (error) { + } + } + } \ No newline at end of file diff --git a/apps/user/templates/winner-template.ts b/apps/user/templates/winner-template.ts new file mode 100644 index 000000000..aed80adf9 --- /dev/null +++ b/apps/user/templates/winner-template.ts @@ -0,0 +1,25 @@ +export class WinnerTemplate { + public getWinnerTemplate(): string { + try { + return ` + + + + + + Winner Template + + +
+
+ 🏆 +
+

Congratulations!

+

You're the Winner of our contest.

+
+ + `; + } catch (error) { + } + } + } \ No newline at end of file diff --git a/apps/user/templates/world-record-template.ts b/apps/user/templates/world-record-template.ts new file mode 100644 index 000000000..ed51f950a --- /dev/null +++ b/apps/user/templates/world-record-template.ts @@ -0,0 +1,28 @@ +export class WorldRecordTemplate { + + public getWorldReccordTemplate(): string { + + try { + return ` + + + + + + Winner Template + + +
+
+ 🏆 +
+

Congratulations!

+

You're the Winner of our contest.

+
+ + `; + + } catch (error) { + } + } + } \ No newline at end of file diff --git a/libs/enum/src/enum.ts b/libs/enum/src/enum.ts index 6a9402c58..911ee2e7c 100644 --- a/libs/enum/src/enum.ts +++ b/libs/enum/src/enum.ts @@ -37,3 +37,10 @@ export enum OrgAgentType { DEDICATED = 1, SHARED = 2 } + +export enum UserCertificateId { + WINNER = 'schemaId1', + PARTICIPANT = 'schemaId2', + ARBITER = 'schemaId3', + WORLD_RECORD = 'schemaId4' +} From d48e19354283678a6064d907a5062bf3d60e6fc7 Mon Sep 17 00:00:00 2001 From: Nishad Date: Thu, 9 Nov 2023 10:23:12 +0530 Subject: [PATCH 06/62] created user_credentials model and migrations file Signed-off-by: Nishad --- .../20231109044921_user_credentials/migration.sql | 13 +++++++++++++ libs/prisma-service/prisma/schema.prisma | 12 ++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 libs/prisma-service/prisma/migrations/20231109044921_user_credentials/migration.sql diff --git a/libs/prisma-service/prisma/migrations/20231109044921_user_credentials/migration.sql b/libs/prisma-service/prisma/migrations/20231109044921_user_credentials/migration.sql new file mode 100644 index 000000000..79c1d23f6 --- /dev/null +++ b/libs/prisma-service/prisma/migrations/20231109044921_user_credentials/migration.sql @@ -0,0 +1,13 @@ +-- CreateTable +CREATE TABLE "user_credentials" ( + "id" TEXT NOT NULL, + "imageUrl" TEXT, + "credentialId" TEXT, + "createDateTime" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "createdBy" TEXT NOT NULL DEFAULT '1', + "lastChangedDateTime" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "lastChangedBy" TEXT NOT NULL DEFAULT '1', + "deletedAt" TIMESTAMP(6), + + CONSTRAINT "user_credentials_pkey" PRIMARY KEY ("id") +); diff --git a/libs/prisma-service/prisma/schema.prisma b/libs/prisma-service/prisma/schema.prisma index d2e8cb680..784019bdc 100644 --- a/libs/prisma-service/prisma/schema.prisma +++ b/libs/prisma-service/prisma/schema.prisma @@ -48,6 +48,18 @@ model user_activity { user user @relation(fields: [userId], references: [id]) } +model user_credentials { + id String @id @default(uuid()) + imageUrl String? + credentialId String? + createDateTime DateTime @default(now()) @db.Timestamptz(6) + createdBy String @default("1") + lastChangedDateTime DateTime @default(now()) @db.Timestamptz(6) + lastChangedBy String @default("1") + deletedAt DateTime? @db.Timestamp(6) +} + + model org_roles { id Int @id @default(autoincrement()) name String @unique From b4078831a1cbc712dcf46fe39cf6c82e983cd181 Mon Sep 17 00:00:00 2001 From: tipusinghaw Date: Thu, 9 Nov 2023 18:05:56 +0530 Subject: [PATCH 07/62] feat: added s3 bucket for file upload Signed-off-by: tipusinghaw --- .../src/issuance/interfaces/index.ts | 2 +- .../src/issuance/issuance.controller.ts | 107 +++----- .../src/issuance/issuance.module.ts | 3 +- .../src/issuance/issuance.service.ts | 7 - .../interfaces/issuance.interfaces.ts | 2 +- apps/issuance/src/issuance.controller.ts | 8 +- apps/issuance/src/issuance.module.ts | 3 +- apps/issuance/src/issuance.service.ts | 27 +- libs/aws/src/aws.module.ts | 8 + libs/aws/src/aws.service.spec.ts | 18 ++ libs/aws/src/aws.service.ts | 55 ++++ libs/aws/src/index.ts | 2 + libs/aws/tsconfig.lib.json | 9 + libs/common/src/common.service.ts | 16 -- nest-cli.json | 9 + package-lock.json | 253 ++++++++++++++++++ package.json | 4 +- tsconfig.json | 6 + 18 files changed, 414 insertions(+), 125 deletions(-) create mode 100644 libs/aws/src/aws.module.ts create mode 100644 libs/aws/src/aws.service.spec.ts create mode 100644 libs/aws/src/aws.service.ts create mode 100644 libs/aws/src/index.ts create mode 100644 libs/aws/tsconfig.lib.json diff --git a/apps/api-gateway/src/issuance/interfaces/index.ts b/apps/api-gateway/src/issuance/interfaces/index.ts index b6c8b0307..f0a30f22e 100644 --- a/apps/api-gateway/src/issuance/interfaces/index.ts +++ b/apps/api-gateway/src/issuance/interfaces/index.ts @@ -64,6 +64,6 @@ export interface FileExportResponse { export interface RequestPayload { credDefId: string; - filePath: string; + fileKey: string; fileName: string; } \ No newline at end of file diff --git a/apps/api-gateway/src/issuance/issuance.controller.ts b/apps/api-gateway/src/issuance/issuance.controller.ts index 84c94d40b..48b9ba309 100644 --- a/apps/api-gateway/src/issuance/issuance.controller.ts +++ b/apps/api-gateway/src/issuance/issuance.controller.ts @@ -15,8 +15,8 @@ import { Param, UseFilters, Header, - UseInterceptors, - UploadedFile + UploadedFile, + UseInterceptors } from '@nestjs/common'; import { ApiTags, @@ -49,10 +49,10 @@ import { OrgRolesGuard } from '../authz/guards/org-roles.guard'; import { CustomExceptionFilter } from 'apps/api-gateway/common/exception-handler'; import { ImageServiceService } from '@credebl/image-service'; import { FileExportResponse, RequestPayload } from './interfaces'; +import { AwsService } from '@credebl/aws'; import { FileInterceptor } from '@nestjs/platform-express'; -import { extname } from 'path'; -import * as fs from 'fs'; -import { multerCSVOptions } from '../config/multer.config'; +import { v4 as uuidv4 } from 'uuid'; +import { RpcException } from '@nestjs/microservices'; @Controller() @UseFilters(CustomExceptionFilter) @ApiTags('credentials') @@ -63,6 +63,7 @@ export class IssuanceController { constructor( private readonly issueCredentialService: IssuanceService, private readonly imageServiceService: ImageServiceService, + private readonly awsService: AwsService, private readonly commonService: CommonService ) { } @@ -186,29 +187,6 @@ export class IssuanceController { } } - @Get('/orgs/:orgId/:path/path') - @ApiOperation({ - summary: `readCSV file`, - description: `readCsv file` - }) - @ApiResponse({ status: 200, description: 'Success', type: ApiResponseDto }) - async readFile( - @User() user: IUserRequest, - @Param('path') path: string, - @Param('orgId') orgId: number, - - @Res() res: Response - ): Promise { - - const getCredentialDetails = await this.issueCredentialService.readCsvFile(path, orgId); - - const finalResponse: IResponseType = { - statusCode: HttpStatus.OK, - message: ResponseMessages.issuance.success.fetch, - data: getCredentialDetails.response - }; - return res.status(HttpStatus.OK).json(finalResponse); - } @Post('/orgs/:orgId/bulk/upload') @Roles(OrgRoles.OWNER, OrgRoles.ADMIN, OrgRoles.ISSUER, OrgRoles.VERIFIER) @@ -245,51 +223,44 @@ export class IssuanceController { }, required: true }) - @UseInterceptors(FileInterceptor('file', multerCSVOptions)) + @UseInterceptors(FileInterceptor('file')) async importAndPreviewDataForIssuance( @Query('credDefId') credentialDefinitionId: string, @UploadedFile() file: Express.Multer.File, @Param('orgId') orgId: number, @Res() res: Response ): Promise { - if (file) { - this.logger.log(`file:${file.path}`); - this.logger.log(`Uploaded file : ${file.filename}`); - const timestamp = Math.floor(Date.now() / 1000); - const ext = extname(file.filename); - const parts = file.filename.split('-'); - // eslint-disable-next-line prefer-destructuring - const resultString = parts[0]; - const newFilename = `${resultString}-${timestamp}${ext}`; - this.logger.log(`newFilename file : ${newFilename}`); - //Testing on dev - fs.rename( - `${process.env.PWD}/uploadedFiles/import/${file.filename}`, - `${process.env.PWD}/uploadedFiles/import/${newFilename}`, - async (err: any) => { - if (err) { - throw err; - } + try { + if (file) { + const fileKey: string = uuidv4(); + try { + + await this.awsService.uploadCsvFile(fileKey, file?.buffer); + + } catch (error) { + + throw new RpcException(error.response ? error.response : error); + } - ); - - const reqPayload: RequestPayload = { - credDefId: credentialDefinitionId, - filePath: `${process.env.PWD}/uploadedFiles/import/${newFilename}`, - fileName: newFilename - }; - this.logger.log(`reqPayload::::::${JSON.stringify(reqPayload)}`); - const importCsvDetails = await this.issueCredentialService.importCsv( - reqPayload - ); - const finalResponse: IResponseType = { - statusCode: HttpStatus.CREATED, - message: ResponseMessages.issuance.success.importCSV, - data: importCsvDetails.response - }; - return res.status(HttpStatus.CREATED).json(finalResponse); - - } + const reqPayload: RequestPayload = { + credDefId: credentialDefinitionId, + fileKey, + fileName:file?.filename + }; + this.logger.log(`reqPayload::::::${JSON.stringify(reqPayload)}`); + const importCsvDetails = await this.issueCredentialService.importCsv( + reqPayload + ); + const finalResponse: IResponseType = { + statusCode: HttpStatus.CREATED, + message: ResponseMessages.issuance.success.importCSV, + data: importCsvDetails.response + }; + return res.status(HttpStatus.CREATED).json(finalResponse); + } + } catch (error) { + throw new RpcException(error.response ? error.response : error); + } } @@ -376,7 +347,7 @@ export class IssuanceController { summary: 'bulk issue credential', description: 'bulk issue credential' }) - async issueBulkCredentials(@Param('requestId') requestId: string, @Param('orgId') orgId: number, @Res() res: Response): Promise { + async issueBulkCredentials(@Param('requestId') requestId: string, @Param('orgId') orgId: number, @Res() res: Response): Promise { const bulkIssunaceDetails = await this.issueCredentialService.issueBulkCredential(requestId, orgId); const finalResponse: IResponseType = { statusCode: HttpStatus.CREATED, @@ -493,5 +464,5 @@ export class IssuanceController { } - + } diff --git a/apps/api-gateway/src/issuance/issuance.module.ts b/apps/api-gateway/src/issuance/issuance.module.ts index bde5e1d4d..15d182e93 100644 --- a/apps/api-gateway/src/issuance/issuance.module.ts +++ b/apps/api-gateway/src/issuance/issuance.module.ts @@ -5,6 +5,7 @@ import { IssuanceService } from './issuance.service'; import { CommonService } from '@credebl/common'; import { HttpModule } from '@nestjs/axios'; import { ImageServiceService } from '@credebl/image-service'; +import { AwsService } from '@credebl/aws'; @Module({ imports: [ @@ -20,6 +21,6 @@ import { ImageServiceService } from '@credebl/image-service'; ]) ], controllers: [IssuanceController], - providers: [IssuanceService, ImageServiceService, CommonService] + providers: [IssuanceService, ImageServiceService, CommonService, AwsService] }) export class IssuanceModule { } diff --git a/apps/api-gateway/src/issuance/issuance.service.ts b/apps/api-gateway/src/issuance/issuance.service.ts index 9bbbfd93e..fae42c4ed 100644 --- a/apps/api-gateway/src/issuance/issuance.service.ts +++ b/apps/api-gateway/src/issuance/issuance.service.ts @@ -45,13 +45,6 @@ export class IssuanceService extends BaseService { return this.sendNats(this.issuanceProxy, 'get-issued-credentials-by-credentialDefinitionId', payload); } - readCsvFile(path: string, orgId: number): Promise<{ - response: object; - }> { - const payload = { path, orgId }; - return this.sendNats(this.issuanceProxy, 'read-csv-path', payload); - } - getIssueCredentialWebhook(issueCredentialDto: IssuanceDto, id: number): Promise<{ response: object; }> { diff --git a/apps/issuance/interfaces/issuance.interfaces.ts b/apps/issuance/interfaces/issuance.interfaces.ts index 9ea1d37c1..3df971a30 100644 --- a/apps/issuance/interfaces/issuance.interfaces.ts +++ b/apps/issuance/interfaces/issuance.interfaces.ts @@ -71,7 +71,7 @@ export interface SchemaDetails { } export interface ImportFileDetails { credDefId: string; - filePath: string; + fileKey: string; fileName: string; } diff --git a/apps/issuance/src/issuance.controller.ts b/apps/issuance/src/issuance.controller.ts index 700112d8e..294106091 100644 --- a/apps/issuance/src/issuance.controller.ts +++ b/apps/issuance/src/issuance.controller.ts @@ -31,13 +31,7 @@ export class IssuanceController { const { user, credentialRecordId, orgId } = payload; return this.issuanceService.getIssueCredentialsbyCredentialRecordId(user, credentialRecordId, orgId); } - - @MessagePattern({ cmd: 'read-csv-path' }) - async fetchCsv(payload): Promise { - const { path } = payload; - return this.issuanceService.readCsvPath(path); - } - + @MessagePattern({ cmd: 'webhook-get-issue-credential' }) async getIssueCredentialWebhook(payload: IIssuanceWebhookInterface): Promise { const { createDateTime, connectionId, threadId, protocolVersion, credentialAttributes, orgId } = payload; diff --git a/apps/issuance/src/issuance.module.ts b/apps/issuance/src/issuance.module.ts index fb08b1d09..6201a237f 100644 --- a/apps/issuance/src/issuance.module.ts +++ b/apps/issuance/src/issuance.module.ts @@ -12,6 +12,7 @@ import { BullModule } from '@nestjs/bull'; import { CacheModule } from '@nestjs/cache-manager'; import * as redisStore from 'cache-manager-redis-store'; import { BulkIssuanceProcessor } from './issuance.processor'; +import { AwsService } from '@credebl/aws'; @Module({ imports: [ @@ -32,6 +33,6 @@ import { BulkIssuanceProcessor } from './issuance.processor'; }) ], controllers: [IssuanceController], - providers: [IssuanceService, IssuanceRepository, PrismaService, Logger, OutOfBandIssuance, EmailDto, BulkIssuanceProcessor] + providers: [IssuanceService, IssuanceRepository, PrismaService, Logger, OutOfBandIssuance, EmailDto, BulkIssuanceProcessor, AwsService] }) export class IssuanceModule { } diff --git a/apps/issuance/src/issuance.service.ts b/apps/issuance/src/issuance.service.ts index e480a99e5..92b0c4e09 100644 --- a/apps/issuance/src/issuance.service.ts +++ b/apps/issuance/src/issuance.service.ts @@ -25,8 +25,7 @@ import { orderValues, paginator } from '@credebl/common/common.utils'; import { InjectQueue } from '@nestjs/bull'; import { Queue } from 'bull'; import { FileUploadStatus, FileUploadType } from 'apps/api-gateway/src/enum'; -import { readFileSync } from 'fs'; - +import { AwsService } from '@credebl/aws'; @Injectable() export class IssuanceService { @@ -38,6 +37,7 @@ export class IssuanceService { @Inject(CACHE_MANAGER) private cacheManager: Cache, private readonly outOfBandIssuance: OutOfBandIssuance, private readonly emailData: EmailDto, + private readonly awsService: AwsService, @InjectQueue('bulk-issuance') private bulkIssuanceQueue: Queue ) { } @@ -232,20 +232,6 @@ export class IssuanceService { } } - async readCsvPath(path:string): Promise { - try { - - const csvFile = readFileSync(path); - - this.logger.log(`csvFile----${JSON.stringify(csvFile)}`); - const csvData: string = csvFile.toString(); - return csvData; - } catch (error) { - this.logger.error(`[fecth files] - error in get fetching file detils : ${JSON.stringify(error)}`); - throw new RpcException(error.response ? error.response : error); - } - } - async _getIssueCredentialsbyCredentialRecordId(url: string, apiKey: string): Promise<{ response: string; }> { @@ -533,14 +519,11 @@ export class IssuanceService { this.logger.log(`credDefResponse----${JSON.stringify(credDefResponse)}`); - this.logger.log(`csvFile::::::${JSON.stringify(importFileDetails.filePath)}`); - - // const csvData = await this.commonService.readFileDetails(importFileDetails.filePath); + this.logger.log(`csvFile::::::${JSON.stringify(importFileDetails.fileKey)}`); - const csvFile = readFileSync(importFileDetails.filePath); + const getFileDetails = await this.awsService.getFile(importFileDetails.fileKey); + const csvData: string = getFileDetails.Body.toString(); - this.logger.log(`csvFile----${JSON.stringify(csvFile)}`); - const csvData: string = csvFile.toString(); const parsedData = paParse(csvData, { header: true, skipEmptyLines: true, diff --git a/libs/aws/src/aws.module.ts b/libs/aws/src/aws.module.ts new file mode 100644 index 000000000..1a2a90f39 --- /dev/null +++ b/libs/aws/src/aws.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { AwsService } from './aws.service'; + +@Module({ + providers: [AwsService], + exports: [AwsService] +}) +export class AwsModule {} diff --git a/libs/aws/src/aws.service.spec.ts b/libs/aws/src/aws.service.spec.ts new file mode 100644 index 000000000..f37dab349 --- /dev/null +++ b/libs/aws/src/aws.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AwsService } from './aws.service'; + +describe('AwsService', () => { + let service: AwsService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [AwsService] + }).compile(); + + service = module.get(AwsService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/libs/aws/src/aws.service.ts b/libs/aws/src/aws.service.ts new file mode 100644 index 000000000..531669845 --- /dev/null +++ b/libs/aws/src/aws.service.ts @@ -0,0 +1,55 @@ +import { Injectable } from '@nestjs/common'; +import { RpcException } from '@nestjs/microservices'; +import { S3 } from 'aws-sdk'; + +@Injectable() +export class AwsService { + private s3: S3; + + constructor() { + this.s3 = new S3({ + accessKeyId: process.env.AWS_ACCESS_KEY, + secretAccessKey: process.env.AWS_SECRET_KEY, + region: process.env.AWS_REGION + }); + } + + async uploadCsvFile(key: string, body: unknown): Promise { + const params: AWS.S3.PutObjectRequest = { + Bucket: process.env.AWS_BUCKET, + Key: key, + Body: 'string' === typeof body ? body : body.toString() + }; + + try { + await this.s3.upload(params).promise(); + } catch (error) { + throw new RpcException(error.response ? error.response : error); + } + } + + + async getFile(key: string): Promise { + const params: AWS.S3.GetObjectRequest = { + Bucket: process.env.AWS_BUCKET, + Key: key + }; + try { + return this.s3.getObject(params).promise(); + } catch (error) { + throw new RpcException(error.response ? error.response : error); + } + } + + async deleteFile(bucketName: string, key: string): Promise { + const params: AWS.S3.DeleteObjectRequest = { + Bucket: process.env.AWS_BUCKET, + Key: key + }; + try { + await this.s3.deleteObject(params).promise(); + } catch (error) { + throw new RpcException(error.response ? error.response : error); + } + } +} diff --git a/libs/aws/src/index.ts b/libs/aws/src/index.ts new file mode 100644 index 000000000..182a99dc1 --- /dev/null +++ b/libs/aws/src/index.ts @@ -0,0 +1,2 @@ +export * from './aws.module'; +export * from './aws.service'; diff --git a/libs/aws/tsconfig.lib.json b/libs/aws/tsconfig.lib.json new file mode 100644 index 000000000..4abcb1f0b --- /dev/null +++ b/libs/aws/tsconfig.lib.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "declaration": true, + "outDir": "../../dist/libs/aws" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] +} diff --git a/libs/common/src/common.service.ts b/libs/common/src/common.service.ts index 72ff4a70a..be9faae23 100644 --- a/libs/common/src/common.service.ts +++ b/libs/common/src/common.service.ts @@ -325,21 +325,5 @@ export class CommonService { } catch (error) { throw new BadRequestException('Invalid Credentials'); } - } - - - // eslint-disable-next-line @typescript-eslint/explicit-function-return-type - // readFileDetails(filePath: string) { - // try { - // const csvFile = readFileSync(filePath); - - // this.logger.log(`csvFile----${JSON.stringify(csvFile)}`); - // const csvData: string = csvFile.toString(); - // return csvData; - // } catch (error) { - // throw new RpcException(error.response); - // } - - // } } diff --git a/nest-cli.json b/nest-cli.json index 3bd64fc64..466b6d3a7 100644 --- a/nest-cli.json +++ b/nest-cli.json @@ -250,6 +250,15 @@ "compilerOptions": { "tsConfigPath": "libs/image-service/tsconfig.lib.json" } + }, + "aws": { + "type": "library", + "root": "libs/aws", + "entryFile": "index", + "sourceRoot": "libs/aws/src", + "compilerOptions": { + "tsConfigPath": "libs/aws/tsconfig.lib.json" + } } } } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index e9a37cc22..e78dbdf9d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,6 +33,7 @@ "@types/pdfkit": "^0.12.6", "async-retry": "^1.3.3", "auth0-js": "^9.22.1", + "aws-sdk": "^2.1492.0", "bcrypt": "^5.1.0", "blob-stream": "^0.1.3", "body-parser": "^1.20.1", @@ -3706,6 +3707,79 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/aws-sdk": { + "version": "2.1492.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1492.0.tgz", + "integrity": "sha512-3q17ruBkwb3pL87CHSbRlYiwx1LCq7D7hIjHgZ/5SPeKknkXgkHnD20SD2lC8Nj3xGbpIUhoKXcpDAGgIM5DBA==", + "dependencies": { + "buffer": "4.9.2", + "events": "1.1.1", + "ieee754": "1.1.13", + "jmespath": "0.16.0", + "querystring": "0.2.0", + "sax": "1.2.1", + "url": "0.10.3", + "util": "^0.12.4", + "uuid": "8.0.0", + "xml2js": "0.5.0" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/aws-sdk/node_modules/buffer": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", + "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", + "dependencies": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, + "node_modules/aws-sdk/node_modules/events": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", + "integrity": "sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw==", + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/aws-sdk/node_modules/ieee754": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" + }, + "node_modules/aws-sdk/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "node_modules/aws-sdk/node_modules/sax": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", + "integrity": "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==" + }, + "node_modules/aws-sdk/node_modules/util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, + "node_modules/aws-sdk/node_modules/uuid": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz", + "integrity": "sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/aws-sign2": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", @@ -7627,6 +7701,20 @@ "node": ">=6" } }, + "node_modules/is-generator-function": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", + "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -8535,6 +8623,14 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/jmespath": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz", + "integrity": "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/jose": { "version": "4.14.4", "resolved": "https://registry.npmjs.org/jose/-/jose-4.14.4.tgz", @@ -10999,6 +11095,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==", + "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", + "engines": { + "node": ">=0.4.x" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -13104,11 +13209,25 @@ "punycode": "^2.1.0" } }, + "node_modules/url": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", + "integrity": "sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ==", + "dependencies": { + "punycode": "1.3.2", + "querystring": "0.2.0" + } + }, "node_modules/url-join": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==" }, + "node_modules/url/node_modules/punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==" + }, "node_modules/urlsafe-base64": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/urlsafe-base64/-/urlsafe-base64-1.0.0.tgz", @@ -13650,6 +13769,26 @@ "xml-js": "bin/cli.js" } }, + "node_modules/xml2js": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "engines": { + "node": ">=4.0" + } + }, "node_modules/xmlhttprequest-ssl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz", @@ -16476,6 +16615,72 @@ "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==" }, + "aws-sdk": { + "version": "2.1492.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1492.0.tgz", + "integrity": "sha512-3q17ruBkwb3pL87CHSbRlYiwx1LCq7D7hIjHgZ/5SPeKknkXgkHnD20SD2lC8Nj3xGbpIUhoKXcpDAGgIM5DBA==", + "requires": { + "buffer": "4.9.2", + "events": "1.1.1", + "ieee754": "1.1.13", + "jmespath": "0.16.0", + "querystring": "0.2.0", + "sax": "1.2.1", + "url": "0.10.3", + "util": "^0.12.4", + "uuid": "8.0.0", + "xml2js": "0.5.0" + }, + "dependencies": { + "buffer": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", + "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, + "events": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", + "integrity": "sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw==" + }, + "ieee754": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "sax": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", + "integrity": "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==" + }, + "util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "requires": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, + "uuid": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz", + "integrity": "sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==" + } + } + }, "aws-sign2": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", @@ -19367,6 +19572,14 @@ "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", "dev": true }, + "is-generator-function": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", + "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", + "requires": { + "has-tostringtag": "^1.0.0" + } + }, "is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -20043,6 +20256,11 @@ } } }, + "jmespath": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz", + "integrity": "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==" + }, "jose": { "version": "4.14.4", "resolved": "https://registry.npmjs.org/jose/-/jose-4.14.4.tgz", @@ -21846,6 +22064,11 @@ "side-channel": "^1.0.4" } }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==" + }, "queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -23335,6 +23558,22 @@ "punycode": "^2.1.0" } }, + "url": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", + "integrity": "sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ==", + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + }, + "dependencies": { + "punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==" + } + } + }, "url-join": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", @@ -23763,6 +24002,20 @@ "sax": "^1.2.4" } }, + "xml2js": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", + "requires": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + } + }, + "xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==" + }, "xmlhttprequest-ssl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz", diff --git a/package.json b/package.json index dee2d9320..40bf3f152 100755 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "@types/pdfkit": "^0.12.6", "async-retry": "^1.3.3", "auth0-js": "^9.22.1", + "aws-sdk": "^2.1492.0", "bcrypt": "^5.1.0", "blob-stream": "^0.1.3", "body-parser": "^1.20.1", @@ -173,7 +174,8 @@ "^@credebl/user-org-roles(|/.*)$": "/libs/user-org-roles/src/$1", "^y/user-activity(|/.*)$": "/libs/user-activity/src/$1", "^@app/supabase(|/.*)$": "/libs/supabase/src/$1", - "^@credebl/image-service(|/.*)$": "/libs/image-service/src/$1" + "^@credebl/image-service(|/.*)$": "/libs/image-service/src/$1", + "^@credebl/aws(|/.*)$": "/libs/aws/src/$1" } } } diff --git a/tsconfig.json b/tsconfig.json index fb258944f..7926469d9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -106,6 +106,12 @@ ], "@credebl/image-service/*": [ "libs/image-service/src/*" + ], + "@credebl/aws": [ + "libs/aws/src" + ], + "@credebl/aws/*": [ + "libs/aws/src/*" ] } }, From 3b3adce7ecdb5688ea1ced48c6016f642bc408d8 Mon Sep 17 00:00:00 2001 From: bhavanakarwade Date: Thu, 9 Nov 2023 18:34:20 +0530 Subject: [PATCH 08/62] feat: share user certificate Signed-off-by: bhavanakarwade --- .../src/user/dto/share-certificate.dto.ts | 28 ++++------ apps/api-gateway/src/user/user.controller.ts | 4 +- apps/user/interfaces/user.interface.ts | 3 +- apps/user/repositories/user.repository.ts | 56 ++++++++++++++++--- apps/user/src/user.service.ts | 14 ++++- apps/user/templates/arbiter-template.ts | 45 +++++++-------- apps/user/templates/participant-template.ts | 45 +++++++-------- apps/user/templates/winner-template.ts | 41 +++++++------- 8 files changed, 132 insertions(+), 104 deletions(-) diff --git a/apps/api-gateway/src/user/dto/share-certificate.dto.ts b/apps/api-gateway/src/user/dto/share-certificate.dto.ts index 5e03522f8..ba09f8d3d 100644 --- a/apps/api-gateway/src/user/dto/share-certificate.dto.ts +++ b/apps/api-gateway/src/user/dto/share-certificate.dto.ts @@ -1,31 +1,23 @@ -import { IsArray, IsNotEmpty, IsString } from 'class-validator'; - +import { IsNotEmpty, IsObject } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; -class AttributeValue { - - @IsString() - @IsNotEmpty({ message: 'name is required.' }) - name: string; - - @IsString() - @IsNotEmpty({ message: 'winner' }) - userType: string; -} - export class CreateUserCertificateDto { - schemaId: string; + @ApiProperty() + credentialId: string; + + @ApiProperty() + schemaId: string; @ApiProperty({ 'example': [ { - name: 'John Doe', - userType: 'winner' + name: 'name', + value: 'value' } ] }) - @IsArray({ message: 'attributes must be an array' }) + @IsObject({ message: 'attributes must be a valid object' }) @IsNotEmpty({ message: 'please provide valid attributes' }) - attributes: AttributeValue[]; + attributes: object[]; } diff --git a/apps/api-gateway/src/user/user.controller.ts b/apps/api-gateway/src/user/user.controller.ts index f775df913..c6dc4d1aa 100644 --- a/apps/api-gateway/src/user/user.controller.ts +++ b/apps/api-gateway/src/user/user.controller.ts @@ -252,9 +252,7 @@ export class UserController { statusCode: HttpStatus.CREATED, message: invitationRes.response }; - return res.status(HttpStatus.CREATED).json(finalResponse); - } @Post('/certificate') @@ -269,7 +267,7 @@ export class UserController { const userCertificateDetails = await this.userService.shareUserCertificate(shareUserCredentials); const finalResponse: IResponseType = { statusCode: HttpStatus.CREATED, - message: 'Certificate created successfully', + message: 'Certificate shared successfully', data: userCertificateDetails.response }; return res.status(HttpStatus.CREATED).json(finalResponse); diff --git a/apps/user/interfaces/user.interface.ts b/apps/user/interfaces/user.interface.ts index 29dc6ff17..41f8307df 100644 --- a/apps/user/interfaces/user.interface.ts +++ b/apps/user/interfaces/user.interface.ts @@ -65,5 +65,6 @@ export interface PlatformSettingsI { export interface ShareUserCertificateI { schemaId: string; - attributes: string[] + credentialId: string; + attributes: object[]; } diff --git a/apps/user/repositories/user.repository.ts b/apps/user/repositories/user.repository.ts index b8b35b53e..00e4048cf 100644 --- a/apps/user/repositories/user.repository.ts +++ b/apps/user/repositories/user.repository.ts @@ -3,6 +3,7 @@ import { Injectable, Logger, NotFoundException } from '@nestjs/common'; import { PlatformSettingsI, + ShareUserCertificateI, UpdateUserProfile, UserEmailVerificationDto, UserI, @@ -438,6 +439,50 @@ export class UserRepository { return { totalPages, users }; } + async getWinnerAttributesBySchemaId(shareUserCertificate: ShareUserCertificateI): Promise { + try { + const getWinnerAttributes = await this.prisma.schema.findFirst({ + where: { + schemaLedgerId: shareUserCertificate.schemaId + } + }); + return getWinnerAttributes; + } catch (error) { + this.logger.error(`checkSchemaExist:${JSON.stringify(error)}`); + throw new InternalServerErrorException(error); + } + } + + async getParticipantAttributesBySchemaId(shareUserCertificate: ShareUserCertificateI): Promise { + try { + const getParticipantAttributes = await this.prisma.schema.findFirst({ + where: { + schemaLedgerId: shareUserCertificate.schemaId + } + }); + + return getParticipantAttributes; + + } catch (error) { + this.logger.error(`checkSchemaExist:${JSON.stringify(error)}`); + throw new InternalServerErrorException(error); + } + } + + async getArbiterAttributesBySchemaId(shareUserCertificate: ShareUserCertificateI): Promise { + try { + const getArbiterAttributes = await this.prisma.schema.findFirst({ + where: { + schemaLedgerId: shareUserCertificate.schemaId + } + }); + return getArbiterAttributes; + } catch (error) { + this.logger.error(`checkSchemaExist:${JSON.stringify(error)}`); + throw new InternalServerErrorException(error); + } + } + async checkUniqueUserExist(email: string): Promise { try { return this.prisma.user.findUnique({ @@ -491,7 +536,7 @@ export class UserRepository { } } - /** + /** * * @Body updatePlatformSettings * @returns Update platform settings @@ -519,21 +564,20 @@ export class UserRepository { } } -/** + /** * * @Body updatePlatformSettings * @returns Update ecosystem settings */ - async updateEcosystemSettings(eosystemKeys: string[], ecosystemObj: object): Promise { + async updateEcosystemSettings(eosystemKeys: string[], ecosystemObj: object): Promise { try { for (const key of eosystemKeys) { - const ecosystemKey = await this.prisma.ecosystem_config.findFirst({ where: { key } }); - + await this.prisma.ecosystem_config.update({ where: { id: ecosystemKey.id @@ -545,7 +589,6 @@ export class UserRepository { } return true; - } catch (error) { this.logger.error(`error: ${JSON.stringify(error)}`); throw new InternalServerErrorException(error); @@ -571,5 +614,4 @@ export class UserRepository { throw new InternalServerErrorException(error); } } - } diff --git a/apps/user/src/user.service.ts b/apps/user/src/user.service.ts index 353be69b9..6e7acd12c 100644 --- a/apps/user/src/user.service.ts +++ b/apps/user/src/user.service.ts @@ -514,13 +514,21 @@ export class UserService { * @returns */ async shareUserCertificate(shareUserCertificate: ShareUserCertificateI): Promise { + const getWinnerAttributes = await this.userRepository.getWinnerAttributesBySchemaId(shareUserCertificate); + const getParticipantAttributes = await this.userRepository.getParticipantAttributesBySchemaId(shareUserCertificate); + const getArbiterAttributes = await this.userRepository.getArbiterAttributesBySchemaId(shareUserCertificate); + + if (!getWinnerAttributes || !getParticipantAttributes || !getArbiterAttributes) { + throw new NotFoundException(ResponseMessages.schema.error.invalidSchemaId); + } + const userWinnerTemplate = new WinnerTemplate(); const userParticipantTemplate = new ParticipantTemplate(); const userArbiterTemplate = new ArbiterTemplate(); - const getWinnerTemplate = await userWinnerTemplate.getWinnerTemplate(); - const getParticipantTemplate = await userParticipantTemplate.getParticipantTemplate(); - const getArbiterTemplate = await userArbiterTemplate.getArbiterTemplate(); + const getWinnerTemplate = await userWinnerTemplate.getWinnerTemplate(getWinnerAttributes); + const getParticipantTemplate = await userParticipantTemplate.getParticipantTemplate(getParticipantAttributes); + const getArbiterTemplate = await userArbiterTemplate.getArbiterTemplate(getArbiterAttributes); if (shareUserCertificate.schemaId === UserCertificateId.WINNER) { return getWinnerTemplate; diff --git a/apps/user/templates/arbiter-template.ts b/apps/user/templates/arbiter-template.ts index f13fab108..29eac7a2e 100644 --- a/apps/user/templates/arbiter-template.ts +++ b/apps/user/templates/arbiter-template.ts @@ -1,28 +1,23 @@ export class ArbiterTemplate { - - public getArbiterTemplate(): string { - - try { - return ` - - - - - - Winner Template - - -
-
- 🏆 + public getArbiterTemplate(attributes: object): string { + try { + return ` + + + + + Arbiter Template + + +
+
👩‍⚖️
+

Thank You, ${attributes}!

+

Your role as ${attributes} is essential in our contest.

-

Congratulations!

-

You're the Winner of our contest.

-
- - `; - - } catch (error) { - } + + `; + } catch (error) { + + } } - } \ No newline at end of file +} diff --git a/apps/user/templates/participant-template.ts b/apps/user/templates/participant-template.ts index 15d3ab074..84b85eff9 100644 --- a/apps/user/templates/participant-template.ts +++ b/apps/user/templates/participant-template.ts @@ -1,27 +1,22 @@ export class ParticipantTemplate { - - public getParticipantTemplate(): string { - - try { - return ` - - - - - - Participant Template - - -
-

John Doe

-

Participant ID: 12345

-

Email: john@example.com

- -
- - `; - - } catch (error) { - } + public getParticipantTemplate(attributes: object): string { + try { + return ` + + + + + Participant Template + + +
+
🎉
+

Thank You, ${attributes}!

+

You're a valued ${attributes} in our contest.

+
+ + `; + } catch (error) { + } } - } \ No newline at end of file +} diff --git a/apps/user/templates/winner-template.ts b/apps/user/templates/winner-template.ts index aed80adf9..620595072 100644 --- a/apps/user/templates/winner-template.ts +++ b/apps/user/templates/winner-template.ts @@ -1,25 +1,22 @@ export class WinnerTemplate { - public getWinnerTemplate(): string { - try { - return ` - - - - - - Winner Template - - -
-
- 🏆 + public getWinnerTemplate(attributes: object): string { + try { + return ` + + + + + Winner Template + + +
+
🏆
+

Congratulations, ${attributes}!

+

You're the ${attributes} of our contest.

-

Congratulations!

-

You're the Winner of our contest.

-
- - `; - } catch (error) { - } + + `; + } catch (error) { + } } - } \ No newline at end of file +} From 850541804261f8e018bfed19de3965c4623c2c26 Mon Sep 17 00:00:00 2001 From: Nishad Date: Fri, 10 Nov 2023 13:00:47 +0530 Subject: [PATCH 09/62] included pool database url in schema prisma Signed-off-by: Nishad --- libs/prisma-service/prisma/schema.prisma | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libs/prisma-service/prisma/schema.prisma b/libs/prisma-service/prisma/schema.prisma index d2e8cb680..fcef6ebce 100644 --- a/libs/prisma-service/prisma/schema.prisma +++ b/libs/prisma-service/prisma/schema.prisma @@ -4,7 +4,8 @@ generator client { datasource db { provider = "postgresql" - url = env("DATABASE_URL") + url = env("POOL_DATABASE_URL") + directUrl = env("DATABASE_URL") } model user { From 4d6c3cc10350a7511a2c07cd7dc094fc7916330a Mon Sep 17 00:00:00 2001 From: tipusinghaw Date: Fri, 10 Nov 2023 16:15:21 +0530 Subject: [PATCH 10/62] fix: download CSV issue Signed-off-by: tipusinghaw --- apps/api-gateway/src/main.ts | 4 ++-- apps/issuance/src/issuance.service.ts | 14 ++++++++------ libs/aws/src/aws.service.ts | 2 +- libs/common/src/response-messages/index.ts | 4 +++- 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/apps/api-gateway/src/main.ts b/apps/api-gateway/src/main.ts index 0655b3d68..e26eba808 100644 --- a/apps/api-gateway/src/main.ts +++ b/apps/api-gateway/src/main.ts @@ -43,12 +43,12 @@ async function bootstrap(): Promise { app.use(express.static('uploadedFiles/holder-profile')); app.use(express.static('uploadedFiles/org-logo')); app.use(express.static('uploadedFiles/tenant-logo')); - app.use(express.static('app/uploadedFiles/exports')); + app.use(express.static('uploadedFiles/exports')); app.use(express.static('resources')); app.use(express.static('genesis-file')); app.use(express.static('invoice-pdf')); app.use(express.static('uploadedFiles/bulk-verification-templates')); - app.use(express.static('app/uploadedFiles/import')); + app.use(express.static('uploadedFiles/import')); app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true })); await app.listen(process.env.API_GATEWAY_PORT, `${process.env.API_GATEWAY_HOST}`); diff --git a/apps/issuance/src/issuance.service.ts b/apps/issuance/src/issuance.service.ts index fa06e3d61..b3ea19517 100644 --- a/apps/issuance/src/issuance.service.ts +++ b/apps/issuance/src/issuance.service.ts @@ -499,7 +499,6 @@ export class IssuanceService { // https required to download csv from frontend side const filePathToDownload = `${process.env.API_GATEWAY_PROTOCOL_SECURE}://${process.env.UPLOAD_LOGO_HOST}/${fileName}`; - return { fileContent: filePathToDownload, fileName: processedFileName @@ -574,8 +573,8 @@ export class IssuanceService { this.logger.error(`error in validating credentials : ${error}`); throw new RpcException(error.response); } finally { + // await this.awsService.deleteFile(importFileDetails.fileKey); // this.logger.error(`Deleted uploaded file after processing.`); - // await deleteFile(importFileDetails.filePath); } } @@ -586,6 +585,9 @@ export class IssuanceService { try { if ('' !== requestId.trim()) { const cachedData = await this.cacheManager.get(requestId); + if (!cachedData) { + throw new NotFoundException(ResponseMessages.issuance.error.emptyFileData); + } if (cachedData === undefined || null) { throw new BadRequestException(ResponseMessages.issuance.error.previewCachedData); } @@ -608,7 +610,7 @@ export class IssuanceService { getAllfileDetails: PreviewRequest ): Promise { try { - + const fileData = await this.issuanceRepository.getFileDetailsByFileId(fileId, getAllfileDetails); const fileResponse = { @@ -624,7 +626,7 @@ export class IssuanceService { if (0 !== fileData.fileCount) { return fileResponse; } else { - throw new NotFoundException(ResponseMessages.issuance.error.notFound); + throw new NotFoundException(ResponseMessages.issuance.error.fileNotFound); } } catch (error) { @@ -638,7 +640,7 @@ export class IssuanceService { getAllfileDetails: PreviewRequest ): Promise { try { - + const fileDetails = await this.issuanceRepository.getAllFileDetails(orgId, getAllfileDetails); const fileResponse = { totalItems: fileDetails.fileCount, @@ -720,7 +722,7 @@ export class IssuanceService { ); }); - return 'Process completed for bulk issuance'; + return 'Process initiated for bulk issuance'; } catch (error) { fileUpload.status = FileUploadStatus.interrupted; this.logger.error(`error in issueBulkCredential : ${error}`); diff --git a/libs/aws/src/aws.service.ts b/libs/aws/src/aws.service.ts index 531669845..810223aa7 100644 --- a/libs/aws/src/aws.service.ts +++ b/libs/aws/src/aws.service.ts @@ -41,7 +41,7 @@ export class AwsService { } } - async deleteFile(bucketName: string, key: string): Promise { + async deleteFile(key: string): Promise { const params: AWS.S3.DeleteObjectRequest = { Bucket: process.env.AWS_BUCKET, Key: key diff --git a/libs/common/src/response-messages/index.ts b/libs/common/src/response-messages/index.ts index d7a08c862..b0624a324 100644 --- a/libs/common/src/response-messages/index.ts +++ b/libs/common/src/response-messages/index.ts @@ -179,7 +179,9 @@ export const ResponseMessages = { emailSend: 'Unable to send email to the user', previewFile: 'Error while fetching file details', previewCachedData: 'Error while fetching cached data', - cacheTimeOut: 'Timeout for reviewing data, re-upload your file and generate new request.' + emptyFileData: 'File details does not exit or removed', + cacheTimeOut: 'Timeout for reviewing data, re-upload your file and generate new request', + fileNotFound: 'File details not found' } }, verification: { From 2bb56ac0114cb73051189ad1bbad7830d0b2d33b Mon Sep 17 00:00:00 2001 From: bhavanakarwade Date: Fri, 10 Nov 2023 16:45:55 +0530 Subject: [PATCH 11/62] feat: share user certificate Signed-off-by: bhavanakarwade --- .../src/user/dto/share-certificate.dto.ts | 41 ++--- apps/api-gateway/src/user/user.controller.ts | 134 ++++++++++------ apps/user/interfaces/user.interface.ts | 7 +- apps/user/repositories/user.repository.ts | 47 ++---- apps/user/src/user.controller.ts | 6 +- apps/user/src/user.service.ts | 143 ++++++++++-------- apps/user/templates/winner-template.ts | 18 ++- libs/enum/src/enum.ts | 8 +- .../src/image-service.service.ts | 4 +- 9 files changed, 227 insertions(+), 181 deletions(-) diff --git a/apps/api-gateway/src/user/dto/share-certificate.dto.ts b/apps/api-gateway/src/user/dto/share-certificate.dto.ts index ba09f8d3d..8b4b29e3a 100644 --- a/apps/api-gateway/src/user/dto/share-certificate.dto.ts +++ b/apps/api-gateway/src/user/dto/share-certificate.dto.ts @@ -1,23 +1,30 @@ -import { IsNotEmpty, IsObject } from 'class-validator'; +import { IsArray, IsNotEmpty, IsString } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; +interface Attribute { + name: string; + value: string; +} export class CreateUserCertificateDto { + @ApiProperty() + @IsNotEmpty({ message: 'Please provide valid schemaId' }) + @IsString({ message: 'credentialId should be string' }) + credentialId: string; - @ApiProperty() - credentialId: string; - - @ApiProperty() - schemaId: string; + @ApiProperty({ example: 'SchemaId' }) + @IsNotEmpty({ message: 'Please provide valid schemaId' }) + @IsString({ message: 'schemaId should be string' }) + schemaId: string; - @ApiProperty({ - 'example': [ - { - name: 'name', - value: 'value' - } - ] - }) - @IsObject({ message: 'attributes must be a valid object' }) - @IsNotEmpty({ message: 'please provide valid attributes' }) - attributes: object[]; + @ApiProperty({ + example: [ + { + name: 'name', + value: 'value' + } + ] + }) + @IsArray({ message: 'attributes must be a valid array' }) + @IsNotEmpty({ message: 'please provide valid attributes' }) + attributes: Attribute[]; } diff --git a/apps/api-gateway/src/user/user.controller.ts b/apps/api-gateway/src/user/user.controller.ts index c6dc4d1aa..b3caf5745 100644 --- a/apps/api-gateway/src/user/user.controller.ts +++ b/apps/api-gateway/src/user/user.controller.ts @@ -1,4 +1,17 @@ -import { Controller, Post, Put, Body, Param, UseFilters, Res, HttpStatus, BadRequestException, Get, Query, UseGuards } from '@nestjs/common'; +import { + Controller, + Post, + Put, + Body, + Param, + UseFilters, + Res, + HttpStatus, + BadRequestException, + Get, + Query, + UseGuards +} from '@nestjs/common'; import { UserService } from './user.service'; import { ApiBearerAuth, @@ -41,13 +54,16 @@ import { CreateUserCertificateDto } from './dto/share-certificate.dto'; @ApiUnauthorizedResponse({ status: 401, description: 'Unauthorized', type: UnauthorizedErrorDto }) @ApiForbiddenResponse({ status: 403, description: 'Forbidden', type: ForbiddenErrorDto }) export class UserController { - constructor(private readonly userService: UserService, private readonly commonService: CommonService) { } + constructor( + private readonly userService: UserService, + private readonly commonService: CommonService + ) {} /** - * - * @param user - * @param orgId - * @param res + * + * @param user + * @param orgId + * @param res * @returns Users list of organization */ @Get('/public-profiles') @@ -68,8 +84,11 @@ export class UserController { type: String, required: false }) - async get(@User() user: IUserRequestInterface, @Query() getAllUsersDto: GetAllUsersDto, @Res() res: Response): Promise { - + async get( + @User() user: IUserRequestInterface, + @Query() getAllUsersDto: GetAllUsersDto, + @Res() res: Response + ): Promise { const users = await this.userService.get(getAllUsersDto); const finalResponse: IResponseType = { statusCode: HttpStatus.OK, @@ -100,7 +119,6 @@ export class UserController { }; return res.status(HttpStatus.OK).json(finalResponse); - } @Get('/profile') @@ -111,7 +129,6 @@ export class UserController { @UseGuards(AuthGuard('jwt')) @ApiBearerAuth() async getProfile(@User() reqUser: user, @Res() res: Response): Promise { - const userData = await this.userService.getProfile(reqUser.id); const finalResponse: IResponseType = { @@ -121,11 +138,13 @@ export class UserController { }; return res.status(HttpStatus.OK).json(finalResponse); - } @Get('/platform-settings') - @ApiOperation({ summary: 'Get all platform and ecosystem settings', description: 'Get all platform and ecosystem settings' }) + @ApiOperation({ + summary: 'Get all platform and ecosystem settings', + description: 'Get all platform and ecosystem settings' + }) @UseGuards(AuthGuard('jwt'), OrgRolesGuard) @Roles(OrgRoles.PLATFORM_ADMIN) @ApiBearerAuth() @@ -139,7 +158,6 @@ export class UserController { }; return res.status(HttpStatus.OK).json(finalResponse); - } @Get('/activity') @@ -150,8 +168,11 @@ export class UserController { @UseGuards(AuthGuard('jwt')) @ApiBearerAuth() @ApiQuery({ name: 'limit', required: true }) - async getUserActivities(@Query('limit') limit: number, @Res() res: Response, @User() reqUser: user): Promise { - + async getUserActivities( + @Query('limit') limit: number, + @Res() res: Response, + @User() reqUser: user + ): Promise { const userDetails = await this.userService.getUserActivities(reqUser.id, limit); const finalResponse: IResponseType = { @@ -163,7 +184,6 @@ export class UserController { return res.status(HttpStatus.OK).json(finalResponse); } - @Get('/org-invitations') @ApiOperation({ summary: 'organization invitations', @@ -191,13 +211,20 @@ export class UserController { type: String, required: false }) - async invitations(@Query() getAllInvitationsDto: GetAllInvitationsDto, @User() reqUser: user, @Res() res: Response): Promise { - + async invitations( + @Query() getAllInvitationsDto: GetAllInvitationsDto, + @User() reqUser: user, + @Res() res: Response + ): Promise { if (!Object.values(Invitation).includes(getAllInvitationsDto.status)) { throw new BadRequestException(ResponseMessages.user.error.invalidInvitationStatus); } - const invitations = await this.userService.invitations(reqUser.id, getAllInvitationsDto.status, getAllInvitationsDto); + const invitations = await this.userService.invitations( + reqUser.id, + getAllInvitationsDto.status, + getAllInvitationsDto + ); const finalResponse: IResponseType = { statusCode: HttpStatus.OK, @@ -206,15 +233,14 @@ export class UserController { }; return res.status(HttpStatus.OK).json(finalResponse); - } /** - * - * @param email - * @param res - * @returns User email check - */ + * + * @param email + * @param res + * @returns User email check + */ @Get('/:email') @ApiOperation({ summary: 'Check user exist', description: 'check user existence' }) async checkUserExist(@Param() emailParam: EmailValidator, @Res() res: Response): Promise { @@ -227,14 +253,13 @@ export class UserController { }; return res.status(HttpStatus.OK).json(finalResponse); - } /** - * - * @param acceptRejectInvitation - * @param reqUser - * @param res + * + * @param acceptRejectInvitation + * @param reqUser + * @param res * @returns Organization invitation status */ @Post('/org-invitations/:invitationId') @@ -244,7 +269,12 @@ export class UserController { }) @UseGuards(AuthGuard('jwt')) @ApiBearerAuth() - async acceptRejectInvitaion(@Body() acceptRejectInvitation: AcceptRejectInvitationDto, @Param('invitationId') invitationId: string, @User() reqUser: user, @Res() res: Response): Promise { + async acceptRejectInvitaion( + @Body() acceptRejectInvitation: AcceptRejectInvitationDto, + @Param('invitationId') invitationId: string, + @User() reqUser: user, + @Res() res: Response + ): Promise { acceptRejectInvitation.invitationId = parseInt(invitationId); const invitationRes = await this.userService.acceptRejectInvitaion(acceptRejectInvitation, reqUser.id); @@ -263,12 +293,17 @@ export class UserController { @ApiResponse({ status: 200, description: 'Success', type: ApiResponseDto }) @UseGuards(AuthGuard('jwt')) @ApiBearerAuth() - async shareUserCertificate (@Body() shareUserCredentials: CreateUserCertificateDto, @Res() res: Response): Promise { - const userCertificateDetails = await this.userService.shareUserCertificate(shareUserCredentials); + async shareUserCertificate( + @Body() shareUserCredentials: CreateUserCertificateDto, + @Res() res: Response + ): Promise { + const imageBuffer = await this.userService.shareUserCertificate(shareUserCredentials); + res.set('Content-Type', 'image/png'); + return res.status(HttpStatus.OK).send(imageBuffer); const finalResponse: IResponseType = { statusCode: HttpStatus.CREATED, message: 'Certificate shared successfully', - data: userCertificateDetails.response + data: await this.userService.shareUserCertificate(shareUserCredentials) }; return res.status(HttpStatus.CREATED).json(finalResponse); } @@ -281,8 +316,11 @@ export class UserController { @ApiResponse({ status: 200, description: 'Success', type: ApiResponseDto }) @ApiBearerAuth() @UseGuards(AuthGuard('jwt')) - async updateUserProfile(@Body() updateUserProfileDto: UpdateUserProfileDto, @User() reqUser: user, @Res() res: Response): Promise { - + async updateUserProfile( + @Body() updateUserProfileDto: UpdateUserProfileDto, + @User() reqUser: user, + @Res() res: Response + ): Promise { const userId = reqUser.id; updateUserProfileDto.id = userId; await this.userService.updateUserProfile(updateUserProfileDto); @@ -292,14 +330,17 @@ export class UserController { message: ResponseMessages.user.success.update }; return res.status(HttpStatus.OK).json(finalResponse); - } @Put('/password/:email') @ApiOperation({ summary: 'Store user password details', description: 'Store user password details' }) @UseGuards(AuthGuard('jwt')) @ApiBearerAuth() - async addPasskey(@Body() userInfo: AddPasskeyDetails, @Param('email') email: string, @Res() res: Response): Promise { + async addPasskey( + @Body() userInfo: AddPasskeyDetails, + @Param('email') email: string, + @Res() res: Response + ): Promise { const userDetails = await this.userService.addPasskey(email, userInfo); const finalResponse = { statusCode: HttpStatus.OK, @@ -308,15 +349,20 @@ export class UserController { }; return res.status(HttpStatus.OK).json(finalResponse); - } - + @Put('/platform-settings') - @ApiOperation({ summary: 'Update platform and ecosystem settings', description: 'Update platform and ecosystem settings' }) + @ApiOperation({ + summary: 'Update platform and ecosystem settings', + description: 'Update platform and ecosystem settings' + }) @UseGuards(AuthGuard('jwt'), OrgRolesGuard) @Roles(OrgRoles.PLATFORM_ADMIN) @ApiBearerAuth() - async updatePlatformSettings(@Body() platformSettings: UpdatePlatformSettingsDto, @Res() res: Response): Promise { + async updatePlatformSettings( + @Body() platformSettings: UpdatePlatformSettingsDto, + @Res() res: Response + ): Promise { const result = await this.userService.updatePlatformSettings(platformSettings); const finalResponse = { @@ -325,7 +371,5 @@ export class UserController { }; return res.status(HttpStatus.OK).json(finalResponse); - } - -} \ No newline at end of file +} diff --git a/apps/user/interfaces/user.interface.ts b/apps/user/interfaces/user.interface.ts index 41f8307df..fe0be11b7 100644 --- a/apps/user/interfaces/user.interface.ts +++ b/apps/user/interfaces/user.interface.ts @@ -66,5 +66,10 @@ export interface PlatformSettingsI { export interface ShareUserCertificateI { schemaId: string; credentialId: string; - attributes: object[]; + attributes: Attribute[]; } + +export interface Attribute { + [key: string]: string; + label: string + } \ No newline at end of file diff --git a/apps/user/repositories/user.repository.ts b/apps/user/repositories/user.repository.ts index 00e4048cf..484f73bff 100644 --- a/apps/user/repositories/user.repository.ts +++ b/apps/user/repositories/user.repository.ts @@ -13,7 +13,7 @@ import { import { InternalServerErrorException } from '@nestjs/common'; import { PrismaService } from '@credebl/prisma-service'; // eslint-disable-next-line camelcase -import { user } from '@prisma/client'; +import { schema, user } from '@prisma/client'; interface UserQueryOptions { id?: number; // Use the appropriate type based on your data model @@ -439,44 +439,15 @@ export class UserRepository { return { totalPages, users }; } - async getWinnerAttributesBySchemaId(shareUserCertificate: ShareUserCertificateI): Promise { + async getAttributesBySchemaId(shareUserCertificate: ShareUserCertificateI): Promise { try { - const getWinnerAttributes = await this.prisma.schema.findFirst({ - where: { - schemaLedgerId: shareUserCertificate.schemaId - } - }); - return getWinnerAttributes; - } catch (error) { - this.logger.error(`checkSchemaExist:${JSON.stringify(error)}`); - throw new InternalServerErrorException(error); - } - } - - async getParticipantAttributesBySchemaId(shareUserCertificate: ShareUserCertificateI): Promise { - try { - const getParticipantAttributes = await this.prisma.schema.findFirst({ - where: { - schemaLedgerId: shareUserCertificate.schemaId - } - }); - - return getParticipantAttributes; - - } catch (error) { - this.logger.error(`checkSchemaExist:${JSON.stringify(error)}`); - throw new InternalServerErrorException(error); - } - } - - async getArbiterAttributesBySchemaId(shareUserCertificate: ShareUserCertificateI): Promise { - try { - const getArbiterAttributes = await this.prisma.schema.findFirst({ - where: { - schemaLedgerId: shareUserCertificate.schemaId - } - }); - return getArbiterAttributes; + const getAttributes = await this.prisma.schema + .findFirst({ + where: { + schemaLedgerId: shareUserCertificate.schemaId + } + }); + return getAttributes; } catch (error) { this.logger.error(`checkSchemaExist:${JSON.stringify(error)}`); throw new InternalServerErrorException(error); diff --git a/apps/user/src/user.controller.ts b/apps/user/src/user.controller.ts index 2e8078f7b..e17b18aab 100644 --- a/apps/user/src/user.controller.ts +++ b/apps/user/src/user.controller.ts @@ -86,9 +86,9 @@ export class UserController { */ @MessagePattern({ cmd: 'share-user-certificate' }) async shareUserCertificate(payload: { - shareUserCertificate: ShareUserCertificateI; - }): Promise { - return this.userService.shareUserCertificate(payload.shareUserCertificate); + shareUserCredentials: ShareUserCertificateI; + }): Promise { + return this.userService.shareUserCertificate(payload.shareUserCredentials); } /** diff --git a/apps/user/src/user.service.ts b/apps/user/src/user.service.ts index 6e7acd12c..aba1f5be4 100644 --- a/apps/user/src/user.service.ts +++ b/apps/user/src/user.service.ts @@ -27,6 +27,7 @@ import { sendEmail } from '@credebl/common/send-grid-helper-file'; import { user } from '@prisma/client'; import { AddPasskeyDetails, + Attribute, InvitationsI, PlatformSettingsI, ShareUserCertificateI, @@ -44,6 +45,7 @@ import { EcosystemConfigSettings, UserCertificateId } from '@credebl/enum/enum'; import { WinnerTemplate } from '../templates/winner-template'; import { ParticipantTemplate } from '../templates/participant-template'; import { ArbiterTemplate } from '../templates/arbiter-template'; +import * as puppeteer from 'puppeteer'; @Injectable() export class UserService { @@ -217,7 +219,7 @@ export class UserService { const resUser = await this.userRepository.addUserPassword(email, userInfo.password); const userDetails = await this.userRepository.getUserDetails(email); const decryptedPassword = await this.commonService.decryptPassword(userDetails.password); - + if (!resUser) { throw new NotFoundException(ResponseMessages.user.error.invalidEmail); } @@ -226,7 +228,7 @@ export class UserService { password: decryptedPassword }); } else { - const decryptedPassword = await this.commonService.decryptPassword(userInfo.password); + const decryptedPassword = await this.commonService.decryptPassword(userInfo.password); supaUser = await this.supabaseService.getClient().auth.signUp({ email, @@ -316,14 +318,13 @@ export class UserService { async generateToken(email: string, password: string): Promise { try { - const supaInstance = await this.supabaseService.getClient(); this.logger.error(`supaInstance::`, supaInstance); - + const { data, error } = await supaInstance.auth.signInWithPassword({ email, password - }); + }); this.logger.error(`Supa Login Error::`, JSON.stringify(error)); @@ -341,21 +342,16 @@ export class UserService { async getProfile(payload: { id }): Promise { try { const userData = await this.userRepository.getUserById(payload.id); - const ecosystemSettingsList = await this.prisma.ecosystem_config.findMany( - { - where:{ - OR: [ - { key: EcosystemConfigSettings.ENABLE_ECOSYSTEM }, - { key: EcosystemConfigSettings.MULTI_ECOSYSTEM } - ] - } + const ecosystemSettingsList = await this.prisma.ecosystem_config.findMany({ + where: { + OR: [{ key: EcosystemConfigSettings.ENABLE_ECOSYSTEM }, { key: EcosystemConfigSettings.MULTI_ECOSYSTEM }] } - ); + }); for (const setting of ecosystemSettingsList) { userData[setting.key] = 'true' === setting.value; } - + return userData; } catch (error) { this.logger.error(`get user: ${JSON.stringify(error)}`); @@ -509,36 +505,60 @@ export class UserService { } } - /** + /** * * @returns */ - async shareUserCertificate(shareUserCertificate: ShareUserCertificateI): Promise { - const getWinnerAttributes = await this.userRepository.getWinnerAttributesBySchemaId(shareUserCertificate); - const getParticipantAttributes = await this.userRepository.getParticipantAttributesBySchemaId(shareUserCertificate); - const getArbiterAttributes = await this.userRepository.getArbiterAttributesBySchemaId(shareUserCertificate); - - if (!getWinnerAttributes || !getParticipantAttributes || !getArbiterAttributes) { + async shareUserCertificate(shareUserCertificate: ShareUserCertificateI): Promise { + const getAttributes = await this.userRepository.getAttributesBySchemaId(shareUserCertificate); + if (!getAttributes) { throw new NotFoundException(ResponseMessages.schema.error.invalidSchemaId); } - const userWinnerTemplate = new WinnerTemplate(); - const userParticipantTemplate = new ParticipantTemplate(); - const userArbiterTemplate = new ArbiterTemplate(); - - const getWinnerTemplate = await userWinnerTemplate.getWinnerTemplate(getWinnerAttributes); - const getParticipantTemplate = await userParticipantTemplate.getParticipantTemplate(getParticipantAttributes); - const getArbiterTemplate = await userArbiterTemplate.getArbiterTemplate(getArbiterAttributes); - - if (shareUserCertificate.schemaId === UserCertificateId.WINNER) { - return getWinnerTemplate; - } else if (shareUserCertificate.schemaId === UserCertificateId.PARTICIPANT) { - return getParticipantTemplate; - } else if (shareUserCertificate.schemaId === UserCertificateId.ARBITER) { - return getArbiterTemplate; - } else { - throw new NotFoundException(ResponseMessages.schema.error.invalidSchemaId); - } + const attributeArray = []; + let attributeJson = {}; + const attributePromises = shareUserCertificate.attributes.map(async (iterator: Attribute) => { + attributeJson = { + [iterator.name]: iterator.value + }; + attributeArray.push(attributeJson); + }); + await Promise.all(attributePromises); + let template; + + switch (shareUserCertificate.schemaId.split(':')[2]) { + case UserCertificateId.WINNER: + // eslint-disable-next-line no-case-declarations + const userWinnerTemplate = new WinnerTemplate(); + template = await userWinnerTemplate.getWinnerTemplate(attributeArray); + break; + case UserCertificateId.PARTICIPANT: + // eslint-disable-next-line no-case-declarations + const userParticipantTemplate = new ParticipantTemplate(); + template = await userParticipantTemplate.getParticipantTemplate(attributeArray); + break; + case UserCertificateId.ARBITER: + // eslint-disable-next-line no-case-declarations + const userArbiterTemplate = new ArbiterTemplate(); + template = await userArbiterTemplate.getArbiterTemplate(attributeArray); + break; + default: + throw new NotFoundException('error in get attributes'); + } + + const imageBuffer = await this.convertHtmlToImage(template); + return imageBuffer; + } + + async convertHtmlToImage(template: string): Promise { + const browser = await puppeteer.launch(); + const page = await browser.newPage(); + + await page.setContent(template); + const screenshot = await page.screenshot({ path: 'cert1.png' }); + + await browser.close(); + return screenshot; } /** @@ -671,7 +691,7 @@ export class UserService { async updatePlatformSettings(platformSettings: PlatformSettingsI): Promise { try { const platformConfigSettings = await this.userRepository.updatePlatformSettings(platformSettings); - + if (!platformConfigSettings) { throw new BadRequestException(ResponseMessages.user.error.notUpdatePlatformSettings); } @@ -691,7 +711,7 @@ export class UserService { if (0 === eosystemKeys.length) { return ResponseMessages.user.success.platformEcosystemettings; } - + const ecosystemSettings = await this.userRepository.updateEcosystemSettings(eosystemKeys, ecosystemobj); if (!ecosystemSettings) { @@ -699,7 +719,6 @@ export class UserService { } return ResponseMessages.user.success.platformEcosystemettings; - } catch (error) { this.logger.error(`update platform settings: ${JSON.stringify(error)}`); throw new RpcException(error.response ? error.response : error); @@ -707,29 +726,27 @@ export class UserService { } async getPlatformEcosystemSettings(): Promise { - try { + try { + const platformSettings = {}; + const platformConfigSettings = await this.userRepository.getPlatformSettings(); - const platformSettings = {}; - const platformConfigSettings = await this.userRepository.getPlatformSettings(); - - if (!platformConfigSettings) { - throw new BadRequestException(ResponseMessages.user.error.platformSetttingsNotFound); - } - - const ecosystemConfigSettings = await this.userRepository.getEcosystemSettings(); - - if (!ecosystemConfigSettings) { - throw new BadRequestException(ResponseMessages.user.error.ecosystemSetttingsNotFound); - } + if (!platformConfigSettings) { + throw new BadRequestException(ResponseMessages.user.error.platformSetttingsNotFound); + } - platformSettings['platform_config'] = platformConfigSettings; - platformSettings['ecosystem_config'] = ecosystemConfigSettings; + const ecosystemConfigSettings = await this.userRepository.getEcosystemSettings(); - return platformSettings; - - } catch (error) { - this.logger.error(`update platform settings: ${JSON.stringify(error)}`); - throw new RpcException(error.response ? error.response : error); + if (!ecosystemConfigSettings) { + throw new BadRequestException(ResponseMessages.user.error.ecosystemSetttingsNotFound); } + + platformSettings['platform_config'] = platformConfigSettings; + platformSettings['ecosystem_config'] = ecosystemConfigSettings; + + return platformSettings; + } catch (error) { + this.logger.error(`update platform settings: ${JSON.stringify(error)}`); + throw new RpcException(error.response ? error.response : error); + } } -} \ No newline at end of file +} diff --git a/apps/user/templates/winner-template.ts b/apps/user/templates/winner-template.ts index 620595072..8092c1f62 100644 --- a/apps/user/templates/winner-template.ts +++ b/apps/user/templates/winner-template.ts @@ -1,7 +1,10 @@ +import { Attribute } from '../interfaces/user.interface'; + export class WinnerTemplate { - public getWinnerTemplate(attributes: object): string { - try { - return ` + public getWinnerTemplate(attributes: Attribute[]): string { + try { + const name = 0 < attributes.length ? attributes[0].name : ''; + return ` @@ -11,12 +14,11 @@ export class WinnerTemplate {
🏆
-

Congratulations, ${attributes}!

-

You're the ${attributes} of our contest.

+

Congratulations, ${name}!

+

You're the Winner of our contest.

`; - } catch (error) { - } - } + } catch (error) {} + } } diff --git a/libs/enum/src/enum.ts b/libs/enum/src/enum.ts index 911ee2e7c..37bd73879 100644 --- a/libs/enum/src/enum.ts +++ b/libs/enum/src/enum.ts @@ -39,8 +39,8 @@ export enum OrgAgentType { } export enum UserCertificateId { - WINNER = 'schemaId1', - PARTICIPANT = 'schemaId2', - ARBITER = 'schemaId3', - WORLD_RECORD = 'schemaId4' + WINNER = 'Winner', + PARTICIPANT = 'Participant', + ARBITER = 'Arbiter', + WORLD_RECORD = 'WORLD_RECORD' } diff --git a/libs/image-service/src/image-service.service.ts b/libs/image-service/src/image-service.service.ts index c8633642d..61140b982 100644 --- a/libs/image-service/src/image-service.service.ts +++ b/libs/image-service/src/image-service.service.ts @@ -1,9 +1,9 @@ -import { Injectable, Logger, Res } from '@nestjs/common'; +import { Injectable, Logger} from '@nestjs/common'; @Injectable() export class ImageServiceService { - private readonly logger = new Logger("Base64ImageService") + private readonly logger = new Logger("Base64ImageService"); constructor( ) { } From 8e8c9a565f920b01247fa082ae48bb47967e8a4d Mon Sep 17 00:00:00 2001 From: tipusinghaw Date: Fri, 10 Nov 2023 16:56:41 +0530 Subject: [PATCH 12/62] fix: file details API issue Signed-off-by: tipusinghaw --- apps/api-gateway/src/issuance/issuance.controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/api-gateway/src/issuance/issuance.controller.ts b/apps/api-gateway/src/issuance/issuance.controller.ts index e72fac464..ffd50a4bc 100644 --- a/apps/api-gateway/src/issuance/issuance.controller.ts +++ b/apps/api-gateway/src/issuance/issuance.controller.ts @@ -390,7 +390,7 @@ export class IssuanceController { return res.status(HttpStatus.OK).json(finalResponse); } - @Get('/orgs/:orgId/bulk/file-data') + @Get('/orgs/:orgId/:fileId/bulk/file-data') @Roles(OrgRoles.OWNER, OrgRoles.ADMIN, OrgRoles.ISSUER, OrgRoles.VERIFIER) @UseGuards(AuthGuard('jwt'), OrgRolesGuard) @ApiBearerAuth() From 12cf74d011c6f65c0458eaf904e678acb718b9c2 Mon Sep 17 00:00:00 2001 From: tipusinghaw <126460794+tipusinghaw@users.noreply.github.com> Date: Fri, 10 Nov 2023 17:00:39 +0530 Subject: [PATCH 13/62] fix: file details API issue (#254) Signed-off-by: tipusinghaw --- apps/api-gateway/src/issuance/issuance.controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/api-gateway/src/issuance/issuance.controller.ts b/apps/api-gateway/src/issuance/issuance.controller.ts index e72fac464..ffd50a4bc 100644 --- a/apps/api-gateway/src/issuance/issuance.controller.ts +++ b/apps/api-gateway/src/issuance/issuance.controller.ts @@ -390,7 +390,7 @@ export class IssuanceController { return res.status(HttpStatus.OK).json(finalResponse); } - @Get('/orgs/:orgId/bulk/file-data') + @Get('/orgs/:orgId/:fileId/bulk/file-data') @Roles(OrgRoles.OWNER, OrgRoles.ADMIN, OrgRoles.ISSUER, OrgRoles.VERIFIER) @UseGuards(AuthGuard('jwt'), OrgRolesGuard) @ApiBearerAuth() From 3c04521fc8e2267e59f11fac8c0bdfef08c74843 Mon Sep 17 00:00:00 2001 From: pallavicoder Date: Fri, 10 Nov 2023 17:06:38 +0530 Subject: [PATCH 14/62] feat:disallowed email from disposable-email-domains Signed-off-by: pallavicoder --- apps/user/src/user.service.ts | 82 ++-- libs/common/src/common.constant.ts | 419 ++++++++++++++++++++- libs/common/src/response-messages/index.ts | 3 +- 3 files changed, 451 insertions(+), 53 deletions(-) diff --git a/apps/user/src/user.service.ts b/apps/user/src/user.service.ts index a8e25762f..3741681f8 100644 --- a/apps/user/src/user.service.ts +++ b/apps/user/src/user.service.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ import { BadRequestException, ConflictException, @@ -40,7 +41,7 @@ import { SupabaseService } from '@credebl/supabase'; import { UserDevicesRepository } from '../repositories/user-device.repository'; import { v4 as uuidv4 } from 'uuid'; import { EcosystemConfigSettings } from '@credebl/enum/enum'; - +import { DISALLOWED_EMAIL_DOMAIN } from '@credebl/common/common.constant'; @Injectable() export class UserService { constructor( @@ -64,6 +65,16 @@ export class UserService { */ async sendVerificationMail(userEmailVerificationDto: UserEmailVerificationDto): Promise { try { + const { email } = userEmailVerificationDto; + + if ('PROD' === process.env.PLATFORM_PROFILE_MODE) { + // eslint-disable-next-line prefer-destructuring + const domain = email.split('@')[1]; + + if (DISALLOWED_EMAIL_DOMAIN.includes(domain)) { + throw new BadRequestException(ResponseMessages.user.error.InvalidEmailDomain); + } + } const userDetails = await this.userRepository.checkUserExist(userEmailVerificationDto.email); if (userDetails && userDetails.isEmailVerified) { @@ -213,7 +224,7 @@ export class UserService { const resUser = await this.userRepository.addUserPassword(email, userInfo.password); const userDetails = await this.userRepository.getUserDetails(email); const decryptedPassword = await this.commonService.decryptPassword(userDetails.password); - + if (!resUser) { throw new NotFoundException(ResponseMessages.user.error.invalidEmail); } @@ -222,7 +233,7 @@ export class UserService { password: decryptedPassword }); } else { - const decryptedPassword = await this.commonService.decryptPassword(userInfo.password); + const decryptedPassword = await this.commonService.decryptPassword(userInfo.password); supaUser = await this.supabaseService.getClient().auth.signUp({ email, @@ -312,14 +323,13 @@ export class UserService { async generateToken(email: string, password: string): Promise { try { - const supaInstance = await this.supabaseService.getClient(); this.logger.error(`supaInstance::`, supaInstance); - + const { data, error } = await supaInstance.auth.signInWithPassword({ email, password - }); + }); this.logger.error(`Supa Login Error::`, JSON.stringify(error)); @@ -337,21 +347,16 @@ export class UserService { async getProfile(payload: { id }): Promise { try { const userData = await this.userRepository.getUserById(payload.id); - const ecosystemSettingsList = await this.prisma.ecosystem_config.findMany( - { - where:{ - OR: [ - { key: EcosystemConfigSettings.ENABLE_ECOSYSTEM }, - { key: EcosystemConfigSettings.MULTI_ECOSYSTEM } - ] - } + const ecosystemSettingsList = await this.prisma.ecosystem_config.findMany({ + where: { + OR: [{ key: EcosystemConfigSettings.ENABLE_ECOSYSTEM }, { key: EcosystemConfigSettings.MULTI_ECOSYSTEM }] } - ); + }); for (const setting of ecosystemSettingsList) { userData[setting.key] = 'true' === setting.value; } - + return userData; } catch (error) { this.logger.error(`get user: ${JSON.stringify(error)}`); @@ -635,7 +640,7 @@ export class UserService { async updatePlatformSettings(platformSettings: PlatformSettingsI): Promise { try { const platformConfigSettings = await this.userRepository.updatePlatformSettings(platformSettings); - + if (!platformConfigSettings) { throw new BadRequestException(ResponseMessages.user.error.notUpdatePlatformSettings); } @@ -655,7 +660,7 @@ export class UserService { if (0 === eosystemKeys.length) { return ResponseMessages.user.success.platformEcosystemettings; } - + const ecosystemSettings = await this.userRepository.updateEcosystemSettings(eosystemKeys, ecosystemobj); if (!ecosystemSettings) { @@ -663,7 +668,6 @@ export class UserService { } return ResponseMessages.user.success.platformEcosystemettings; - } catch (error) { this.logger.error(`update platform settings: ${JSON.stringify(error)}`); throw new RpcException(error.response ? error.response : error); @@ -671,29 +675,27 @@ export class UserService { } async getPlatformEcosystemSettings(): Promise { - try { + try { + const platformSettings = {}; + const platformConfigSettings = await this.userRepository.getPlatformSettings(); - const platformSettings = {}; - const platformConfigSettings = await this.userRepository.getPlatformSettings(); - - if (!platformConfigSettings) { - throw new BadRequestException(ResponseMessages.user.error.platformSetttingsNotFound); - } - - const ecosystemConfigSettings = await this.userRepository.getEcosystemSettings(); - - if (!ecosystemConfigSettings) { - throw new BadRequestException(ResponseMessages.user.error.ecosystemSetttingsNotFound); - } + if (!platformConfigSettings) { + throw new BadRequestException(ResponseMessages.user.error.platformSetttingsNotFound); + } - platformSettings['platform_config'] = platformConfigSettings; - platformSettings['ecosystem_config'] = ecosystemConfigSettings; + const ecosystemConfigSettings = await this.userRepository.getEcosystemSettings(); - return platformSettings; - - } catch (error) { - this.logger.error(`update platform settings: ${JSON.stringify(error)}`); - throw new RpcException(error.response ? error.response : error); + if (!ecosystemConfigSettings) { + throw new BadRequestException(ResponseMessages.user.error.ecosystemSetttingsNotFound); } + + platformSettings['platform_config'] = platformConfigSettings; + platformSettings['ecosystem_config'] = ecosystemConfigSettings; + + return platformSettings; + } catch (error) { + this.logger.error(`update platform settings: ${JSON.stringify(error)}`); + throw new RpcException(error.response ? error.response : error); + } } -} \ No newline at end of file +} diff --git a/libs/common/src/common.constant.ts b/libs/common/src/common.constant.ts index 3ccac0c81..11e0fbe07 100644 --- a/libs/common/src/common.constant.ts +++ b/libs/common/src/common.constant.ts @@ -1,4 +1,3 @@ - export enum CommonConstants { // Error and Success Responses from POST and GET calls RESP_ERR_HTTP_INVALID_HEADER_VALUE = 'ERR_HTTP_INVALID_HEADER_VALUE', @@ -42,7 +41,6 @@ export enum CommonConstants { URL_LEDG_GET_TAA = '/ledger/taa', URL_LEDG_POST_TAA_ACCEPT = '/ledger/taa/accept', - // MESSAGING SERVICES URL_MSG_SEND_MESSAGE = '/connections/#/send-message', URL_MSG_TRUST_PING = '/connections/#/send-ping', @@ -110,7 +108,6 @@ export enum CommonConstants { URL_SEND_OUT_OF_BAND_CREATE_REQUEST = '/proofs/create-request-oob', URL_PROOF_FORM_DATA = '/proofs/#/form-data', - // server or agent URL_SERVER_STATUS = '/status', URL_AGENT_WRITE_DID = '/dids/write', @@ -130,7 +127,6 @@ export enum CommonConstants { ENTITY_ACTION_UPDATE = 'update', ENTITY_ACTION_DELETE = 'delete', - // EVENTS EVENT_AUDIT = 'audit_event', @@ -144,17 +140,16 @@ export enum CommonConstants { DOMAIN_EVENT_USER_ONBOARD = 'User Onboard', DOMAIN_EVENT_WALLET_CREATED = 'Wallet Created', - // (Platform) admin permissions + // (Platform) admin permissions PERMISSION_TENANT_MGMT = 'Tenant Management', PERMISSION_ROLE_MGMT = 'Role Management', PERMISSION_ORG_REPORTS = 'Organization Reports', PERMISSION_TENANT_REPORTS = 'Tenant Reports', - // Tenant permissions + // Tenant permissions PERMISSION_ORG_MGMT = 'Organization Management', PERMISSION_MODIFY_ORG = 'Modify Organizations', - // Roles And Permissions PERMISSION_PLATFORM_MANAGEMENT = 'Platform Management', PERMISSION_USER_MANAGEMENT = 'User Management', @@ -256,7 +251,6 @@ export enum CommonConstants { LOGIN_PASSWORDLESS = 'passwordless', LOGIN_PASSWORD = 'password', - //onBoarding Type ONBOARDING_TYPE_ADMIN = 0, ONBOARDING_TYPE_EXTERNAL = 1, @@ -265,14 +259,13 @@ export enum CommonConstants { // ecosystem config auto endorsement ECOSYSTEM_AUTO_ENDOSEMENT = 'autoEndorsement', - // Network + // Network TESTNET = 'testnet', STAGINGNET = 'stagingnet', BUILDERNET = 'buildernet', MAINNET = 'mainnet', LIVENET = 'livenet', - // Features Id SCHEMA_CREATION = 1, CREATE_CREDENTIAL_DEFINITION = 2, @@ -291,8 +284,9 @@ export enum CommonConstants { TRANSACTION_MULTITENANT_SCHEMA = '/multi-tenancy/schema/#', TRANSACTION_MULTITENANT_CRED_DEF = '/multi-tenancy/credential-definition/#', TRANSACTION_MULTITENANT_SIGN = '/multi-tenancy/transactions/endorse/#', - TRANSACTION_MULTITENANT_SUMBIT = '/multi-tenancy/transactions/write/#' + TRANSACTION_MULTITENANT_SUMBIT = '/multi-tenancy/transactions/write/#', + } export const postgresqlErrorCodes = []; @@ -336,7 +330,6 @@ postgresqlErrorCodes['22012'] = 'division_by_zero'; postgresqlErrorCodes['22005'] = 'error_in_assignment'; postgresqlErrorCodes['2200B'] = 'escape_character_conflict'; - postgresqlErrorCodes['22022'] = 'indicator_overflow'; postgresqlErrorCodes['22015'] = 'interval_field_overflow'; postgresqlErrorCodes['2201E'] = 'invalid_argument_for_logarithm'; @@ -350,3 +343,405 @@ postgresqlErrorCodes['22019'] = 'invalid_escape_character'; postgresqlErrorCodes['22P02'] = 'invalid_datatype'; postgresqlErrorCodes[''] = ''; + +export const DISALLOWED_EMAIL_DOMAIN = [ + '0x01.gq', + '0x01.tk', + '10mail.org', + '10mail.tk', + '33m.co', + '33mail.com', + '3dxtras.com', + '3utilities.com', + '567map.xyz', + '8191.at', + 'aa.am', + 'accountsite.me', + 'acmetoy.com', + 'acusupply.com', + 'adultvidlite.com', + 'aji.kr', + 'anonaddy.com', + 'anonaddy.me', + 'anonbox.net', + 'anyalias.com', + 'asanatest1.us', + 'azzawajalla.store', + 'bajetesik.store', + 'band-freier.de', + 'bandband1.com', + 'bangmadid.store', + 'batikbantul.com', + 'bccto.me', + 'bebekpenyet.buzz', + 'bei.kr', + 'bel.kr', + 'beo.kr', + 'bfo.kr', + 'bgsaddrmwn.me', + 'bho.kr', + 'biasaelho.space', + 'biz.st', + 'biz.tm', + 'bko.kr', + 'blacksong.pw', + 'blueauramassage.com', + 'bounceme.net', + 'bum.net', + 'buwosok.tech', + 'buzzndaraiangop2wae.buzz', + 'byui.me', + 'caboodle.buzz', + 'cad.edu.gr', + 'cempue.online', + 'chickenkiller.com', + 'choirul.host', + 'cid.kr', + 'ciran.xyz', + 'cko.kr', + 'cloudns.asia', + 'cloudns.cc', + 'cloudns.cx', + 'cloudns.nz', + 'com.com', + 'coms.hk', + 'comx.cf', + 'craigslist.org', + 'creo.site', + 'creo.tips', + 'creou.dev', + 'crowdpress.it', + 'cu.cc', + 'cua77.xyz', + 'd3vs.net', + 'dadosa.xyz', + 'danuarte.online', + 'darrels.site', + 'daseus.online', + 'dayatan.host', + 'dbo.kr', + 'ddns.net', + 'ddnsfree.com', + 'deail.com', + 'dedyn.io', + 'defaultdomain.ml', + 'discard-email.cf', + 'dko.kr', + 'dlink.cf', + 'dlink.gq', + 'dlyemail.com', + 'dmtc.dev', + 'dmtc.edu.pl', + 'dmtc.press', + 'dns-cloud.net', + 'dns.navy', + 'dnsabr.com', + 'dnses.ro', + 'doy.kr', + 'drope.ml', + 'dropmail.me', + 'dynu.net', + 'dzalaev-advokat.ru', + 'e4ward.com', + 'ediantenan.site', + 'edu.auction', + 'efo.kr', + 'eho.kr', + 'ely.kr', + 'email-temp.com', + 'emailfake.com', + 'emailfake.ml', + 'emailfreedom.ml', + 'emlhub.com', + 'emlpro.com', + 'emltmp.com', + 'emy.kr', + 'enu.kr', + 'eny.kr', + 'epizy.com', + 'escritossad.net', + 'ese.kr', + 'esy.es', + 'ewa.kr', + 'exi.kr', + 'ezyro.com', + 'fackme.gq', + 'fassagforpresident.ga', + 'firste.ml', + 'flu.cc', + 'foy.kr', + 'fr.nf', + 'freeml.net', + 'gadzooks.buzz', + 'gettrials.com', + 'giize.com', + 'gmail.gr.com', + 'gmeil.me', + 'gok.kr', + 'gotdns.ch', + 'gpa.lu', + 'grigio.cf', + 'guardmail.cf', + 'haddo.eu', + 'heliohost.org', + 'higogoya.com', + 'historial.store', + 'hitechinfo.com', + 'hix.kr', + 'hiz.kr', + 'hmail.us', + 'hopto.org', + 'hostingarif.me', + 'idn.vn', + 'iesco.info', + 'igg.biz', + 'ignorelist.com', + 'iki.kr', + 'ilovemyniggers.club', + 'imouto.pro', + 'info.tm', + 'infos.st', + 'irr.kr', + 'isgre.at', + 'it2-mail.tk', + 'jil.kr', + 'jindmail.club', + 'jto.kr', + 'junnuok.com', + 'justemail.ml', + 'kadokawa.top', + 'kantal.buzz', + 'keitin.site', + 'kentel.buzz', + 'kerl.cf', + 'kerl.gq', + 'kikwet.com', + 'kondomeus.site', + 'kozow.com', + 'kranjingan.store', + 'kranjingan.tech', + 'kranjingans.tech', + 'kro.kr', + 'lal.kr', + 'laste.ml', + 'lbe.kr', + 'legundi.site', + 'lei.kr', + 'likevip.net', + 'liopers.link', + 'lko.co.kr', + 'lko.kr', + 'll47.net', + 'lofteone.ru', + 'lom.kr', + 'longdz.site', + 'longmusic.com', + 'lostandalone.com', + 'loudcannabisapp.com', + 'loy.kr', + 'loyalherceghalom.ml', + 'luk2.com', + 'luksarcenter.ru', + 'luo.kr', + 'lyrics-lagu.me', + 'mail-temp.com', + 'mail0.ga', + 'mailinator.com', + 'mailr.eu', + 'marrone.cf', + 'mbe.kr', + 'mblimbingan.space', + 'mebelnovation.ru', + 'mefound.com', + 'mintemail.com', + 'mishmash.buzz', + 'mko.kr', + 'mlo.kr', + 'mooo.com', + 'motifasidiri.website', + 'mp-j.cf', + 'mp-j.ga', + 'mp-j.gq', + 'mp-j.ml', + 'mp-j.tk', + 'mr-meshkat.com', + 'mrossi.cf', + 'mrossi.gq', + 'mrossi.ml', + 'ms1.email', + 'msdc.co', + 'muabanwin.net', + 'museumplanet.com', + 'my.id', + 'my3mail.cf', + 'my3mail.ga', + 'my3mail.gq', + 'my3mail.ml', + 'my3mail.tk', + 'myddns.me', + 'myeslbookclub.com', + 'mymy.cf', + 'mysafe.ml', + 'mzon.store', + 'n-e.kr', + 'nafko.cf', + 'nctu.me', + 'netmail.tk', + 'netricity.nl', + 'new-mgmt.ga', + 'ngalasmoen.xyz', + 'ngguwokulon.online', + 'njambon.space', + 'nko.kr', + 'now.im', + 'npv.kr', + 'nuo.co.kr', + 'nuo.kr', + 'nut.cc', + 'o-r.kr', + 'oazis.site', + 'obo.kr', + 'ocry.com', + 'office.gy', + 'okezone.bid', + 'one.pl', + 'onlysext.com', + 'oovy.org', + 'oppoesrt.online', + 'orangotango.ml', + 'otherinbox.com', + 'ourhobby.com', + 'owa.kr', + 'owh.ooo', + 'oyu.kr', + 'p-e.kr', + 'pafnuty.com', + 'pandies.space', + 'paqeh.online', + 'pe.hu', + 'petinggiean.tech', + 'peyekkolipi.buzz', + 'poderosamulher.com', + 'poistaa.com', + 'porco.cf', + 'poy.kr', + 'prapto.host', + 'probatelawarizona.com', + 'ptcu.dev', + 'pubgm.website', + 'qbi.kr', + 'qc.to', + 'r-e.kr', + 'ragel.me', + 'rao.kr', + 'reilis.site', + 'rf.gd', + 'ringen.host', + 'rko.kr', + 'rosso.ml', + 'row.kr', + 'rr.nu', + 'rshagor.xyz', + 's-ly.me', + 'safe-mail.gq', + 'sagun.info', + 'samsueng.site', + 'saucent.online', + 'sborra.tk', + 'schwarzmail.ga', + 'seluang.com', + 'sempak.link', + 'sendaljepit.site', + 'sendangagung.online', + 'servegame.com', + 'shp7.cn', + 'siambretta.com', + 'skodaauto.cf', + 'soju.buzz', + 'solidplai.us', + 'somee.com', + 'spamtrap.ro', + 'spymail.one', + 'ssanphone.me', + 'standeight.com', + 'statuspage.ga', + 'steakbeef.site', + 'stonedogdigital.com', + 'stop-my-spam.pp.ua', + 'storeyee.com', + 'sumanan.site', + 'supere.ml', + 'svblog.com', + 'sytes.net', + 'tandy.co', + 'tangtingtung.tech', + 'teml.net', + 'tempembus.buzz', + 'tempremail.cf', + 'tempremail.tk', + 'tgwrzqr.top', + 'thepieter.com', + 'theworkpc.com', + 'thinktimessolve.info', + 'thumoi.com', + 'tko.co.kr', + 'tko.kr', + 'tmo.kr', + 'tmpeml.com', + 'toh.info', + 'toi.kr', + 'tomcrusenono.host', + 'topikurrohman.xyz', + 'tourbalitravel.com', + 'traveldesk.com', + 'tricakesi.store', + 'trillianpro.com', + 'twilightparadox.com', + 'tyrex.cf', + 'uha.kr', + 'uk.to', + 'uko.kr', + 'umy.kr', + 'unaux.com', + 'undo.it', + 'uny.kr', + 'uola.org', + 'upy.kr', + 'urbanban.com', + 'us.to', + 'usa.cc', + 'uu.gl', + 'uvy.kr', + 'uyu.kr', + 'vay.kr', + 'vba.kr', + 'veo.kr', + 'viola.gq', + 'vivoheroes.xyz', + 'vkbags.in', + 'vo.uk', + 'volvo-xc.tk', + 'vuforia.us', + 'wakultimbo.buzz', + 'web.id', + 'weprof.it', + 'werkuldino.buzz', + 'wil.kr', + 'wingkobabat.buzz', + 'x24hr.com', + 'xiaomie.store', + 'xo.uk', + 'xxi2.com', + 'yarien.eu', + 'yawahid.host', + 'ye.vc', + 'yertxenor.tk', + 'yomail.info', + 'yopmail.com', + 'yoqoyyum.space', + 'youdontcare.com', + 'zalvisual.us', + 'zapto.org', + 'ze.cx', + 'zeroe.ml' +]; diff --git a/libs/common/src/response-messages/index.ts b/libs/common/src/response-messages/index.ts index 44797b26c..1b4764515 100644 --- a/libs/common/src/response-messages/index.ts +++ b/libs/common/src/response-messages/index.ts @@ -40,7 +40,8 @@ export const ResponseMessages = { adduser: 'Unable to add user details', verifyEmail: 'The verification link has already been sent to your email address. please verify', emailNotVerified: 'The verification link has already been sent to your email address. please verify', - userNotRegisterd: 'The user has not yet completed the registration process' + userNotRegisterd: 'The user has not yet completed the registration process', + InvalidEmailDomain :'Email from this domain is not allowed' } }, organisation: { From 5f3964cf9927457b2f6a54ddca0a14311e8a223b Mon Sep 17 00:00:00 2001 From: tipusinghaw Date: Fri, 10 Nov 2023 18:35:46 +0530 Subject: [PATCH 15/62] added logger to test issuance Signed-off-by: tipusinghaw --- apps/issuance/src/issuance.service.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/issuance/src/issuance.service.ts b/apps/issuance/src/issuance.service.ts index b3ea19517..80c4cae46 100644 --- a/apps/issuance/src/issuance.service.ts +++ b/apps/issuance/src/issuance.service.ts @@ -685,8 +685,10 @@ export class IssuanceService { `Param 'requestId' is missing from the request.` ); } + this.logger.log(`requestId----${JSON.stringify(requestId)}`); try { const cachedData = await this.cacheManager.get(requestId); + this.logger.log(`cachedData----${JSON.stringify(cachedData)}`); if (cachedData === undefined) { throw new BadRequestException(ResponseMessages.issuance.error.cacheTimeOut); } @@ -748,6 +750,7 @@ export class IssuanceService { error: '', detailError: '' }; + this.logger.log(`jobDetails----${JSON.stringify(jobDetails)}`); fileUploadData.fileUpload = jobDetails.fileUploadId; fileUploadData.fileRow = JSON.stringify(jobDetails); From 5c6636e2eed1b5e531069a4bddd8832363b85db3 Mon Sep 17 00:00:00 2001 From: tipusinghaw <126460794+tipusinghaw@users.noreply.github.com> Date: Fri, 10 Nov 2023 18:39:18 +0530 Subject: [PATCH 16/62] fix: added logger for bulk-issuance (#256) * fix: file details API issue Signed-off-by: tipusinghaw * added logger to test issuance Signed-off-by: tipusinghaw --------- Signed-off-by: tipusinghaw --- apps/issuance/src/issuance.service.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/issuance/src/issuance.service.ts b/apps/issuance/src/issuance.service.ts index b3ea19517..80c4cae46 100644 --- a/apps/issuance/src/issuance.service.ts +++ b/apps/issuance/src/issuance.service.ts @@ -685,8 +685,10 @@ export class IssuanceService { `Param 'requestId' is missing from the request.` ); } + this.logger.log(`requestId----${JSON.stringify(requestId)}`); try { const cachedData = await this.cacheManager.get(requestId); + this.logger.log(`cachedData----${JSON.stringify(cachedData)}`); if (cachedData === undefined) { throw new BadRequestException(ResponseMessages.issuance.error.cacheTimeOut); } @@ -748,6 +750,7 @@ export class IssuanceService { error: '', detailError: '' }; + this.logger.log(`jobDetails----${JSON.stringify(jobDetails)}`); fileUploadData.fileUpload = jobDetails.fileUploadId; fileUploadData.fileRow = JSON.stringify(jobDetails); From f9d13a340afd6a5358b4b4349e7ed6b7651c9b60 Mon Sep 17 00:00:00 2001 From: bhavanakarwade Date: Fri, 10 Nov 2023 18:52:23 +0530 Subject: [PATCH 17/62] implement url generate function Signed-off-by: bhavanakarwade --- apps/api-gateway/src/user/user.controller.ts | 30 ++++++------ apps/api-gateway/src/user/user.module.ts | 3 +- apps/api-gateway/src/user/user.service.ts | 2 +- apps/user/src/user.service.ts | 1 + libs/aws/src/aws.service.ts | 48 ++++++++++++++++++-- 5 files changed, 64 insertions(+), 20 deletions(-) diff --git a/apps/api-gateway/src/user/user.controller.ts b/apps/api-gateway/src/user/user.controller.ts index b3caf5745..65bb13c23 100644 --- a/apps/api-gateway/src/user/user.controller.ts +++ b/apps/api-gateway/src/user/user.controller.ts @@ -10,7 +10,8 @@ import { BadRequestException, Get, Query, - UseGuards + UseGuards, + UploadedFile } from '@nestjs/common'; import { UserService } from './user.service'; import { @@ -47,6 +48,7 @@ import { Roles } from '../authz/decorators/roles.decorator'; import { OrgRolesGuard } from '../authz/guards/org-roles.guard'; import { OrgRoles } from 'libs/org-roles/enums'; import { CreateUserCertificateDto } from './dto/share-certificate.dto'; +import { AwsService } from '@credebl/aws/aws.service'; @UseFilters(CustomExceptionFilter) @Controller('users') @@ -56,7 +58,8 @@ import { CreateUserCertificateDto } from './dto/share-certificate.dto'; export class UserController { constructor( private readonly userService: UserService, - private readonly commonService: CommonService + private readonly commonService: CommonService, + private readonly awsService: AwsService ) {} /** @@ -291,21 +294,22 @@ export class UserController { description: 'Share user certificate' }) @ApiResponse({ status: 200, description: 'Success', type: ApiResponseDto }) - @UseGuards(AuthGuard('jwt')) - @ApiBearerAuth() async shareUserCertificate( @Body() shareUserCredentials: CreateUserCertificateDto, + @UploadedFile() file: Express.Multer.File, @Res() res: Response - ): Promise { + ): Promise { const imageBuffer = await this.userService.shareUserCertificate(shareUserCredentials); - res.set('Content-Type', 'image/png'); - return res.status(HttpStatus.OK).send(imageBuffer); - const finalResponse: IResponseType = { - statusCode: HttpStatus.CREATED, - message: 'Certificate shared successfully', - data: await this.userService.shareUserCertificate(shareUserCredentials) - }; - return res.status(HttpStatus.CREATED).json(finalResponse); + if (file) { + const certificateImageBuffer = imageBuffer.response; + const imageUrl = await this.awsService.uploads3(certificateImageBuffer, 'png', './certificates', 'base64'); + const finalResponse: IResponseType = { + statusCode: HttpStatus.CREATED, + message: 'Certificate url generated successfully', + data: imageUrl + }; + return res.status(HttpStatus.CREATED).json(finalResponse); + } } @Put('/') diff --git a/apps/api-gateway/src/user/user.module.ts b/apps/api-gateway/src/user/user.module.ts index 7d494c009..af71d20af 100644 --- a/apps/api-gateway/src/user/user.module.ts +++ b/apps/api-gateway/src/user/user.module.ts @@ -5,6 +5,7 @@ import { HttpModule } from '@nestjs/axios'; import { Module } from '@nestjs/common'; import { UserController } from './user.controller'; import { UserService } from './user.service'; +import { AwsService } from '@credebl/aws'; @Module({ imports: [ @@ -21,6 +22,6 @@ import { UserService } from './user.service'; ]) ], controllers: [UserController], - providers: [UserService, CommonService] + providers: [UserService, CommonService, AwsService] }) export class UserModule {} diff --git a/apps/api-gateway/src/user/user.service.ts b/apps/api-gateway/src/user/user.service.ts index 5138cd8e7..52a5f9394 100644 --- a/apps/api-gateway/src/user/user.service.ts +++ b/apps/api-gateway/src/user/user.service.ts @@ -53,7 +53,7 @@ export class UserService extends BaseService { async shareUserCertificate( shareUserCredentials: CreateUserCertificateDto - ): Promise<{ response: object }> { + ): Promise<{ response: Buffer }> { const payload = { shareUserCredentials}; return this.sendNats(this.serviceProxy, 'share-user-certificate', payload); } diff --git a/apps/user/src/user.service.ts b/apps/user/src/user.service.ts index aba1f5be4..448d94287 100644 --- a/apps/user/src/user.service.ts +++ b/apps/user/src/user.service.ts @@ -560,6 +560,7 @@ export class UserService { await browser.close(); return screenshot; } + /** * diff --git a/libs/aws/src/aws.service.ts b/libs/aws/src/aws.service.ts index 810223aa7..6b112c59a 100644 --- a/libs/aws/src/aws.service.ts +++ b/libs/aws/src/aws.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@nestjs/common'; +import { BadRequestException, HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { RpcException } from '@nestjs/microservices'; import { S3 } from 'aws-sdk'; @@ -8,12 +8,51 @@ export class AwsService { constructor() { this.s3 = new S3({ - accessKeyId: process.env.AWS_ACCESS_KEY, - secretAccessKey: process.env.AWS_SECRET_KEY, - region: process.env.AWS_REGION + accessKeyId: process.env.AWS_PUBLIC_ACCESS_KEY, + secretAccessKey: process.env.AWS_PUBLIC_SECRET_KEY, + region: process.env.AWS_PUBLIC_REGION }); } + async uploads3( + fileBuffer: Buffer, + ext: string, + pathAWS: string = '', + encoding = 'base64', + filename = 'nftp' + ): Promise { + const timestamp = Date.now(); + await this.s3.putObject( + { + Bucket: process.env.AWS_PUBLIC_BUCKET_NAME, + Key: `${pathAWS}/${encodeURIComponent(filename)}.${timestamp}.${ext}`, + Body: fileBuffer.toString(), + ContentEncoding: encoding + }, + (err) => { + if (err) { + throw new HttpException('An error occurred while uploading the image', HttpStatus.SERVICE_UNAVAILABLE); + } else { + return 'photo is uploaded'; + } + } + ); + + return `https://${process.env.AWS_PUBLIC_BUCKET_NAME}.s3.amazonaws.com/${pathAWS}/${encodeURIComponent( + filename + )}-${timestamp}.${ext}`; + } + + async fileUpload(file: Express.Multer.File): Promise { + const fileExt = file['originalname'].split('.')[file['originalname'].split('.').length - 1]; + if ('image/png' === file['mimetype'] || 'image/jpg' === file['mimetype'] || 'image/jpeg' === file['mimetype']) { + const awsResponse = await this.uploads3(file['buffer'], fileExt, file['mimetype'], 'images'); + return awsResponse; + } else { + throw new BadRequestException('File format should be PNG,JPG,JPEG'); + } + } + async uploadCsvFile(key: string, body: unknown): Promise { const params: AWS.S3.PutObjectRequest = { Bucket: process.env.AWS_BUCKET, @@ -28,7 +67,6 @@ export class AwsService { } } - async getFile(key: string): Promise { const params: AWS.S3.GetObjectRequest = { Bucket: process.env.AWS_BUCKET, From 7fdc41587ee67dcdd5f635a74a85c11c72d2a738 Mon Sep 17 00:00:00 2001 From: tipusinghaw Date: Fri, 10 Nov 2023 19:39:18 +0530 Subject: [PATCH 18/62] added logger to test issuance Signed-off-by: tipusinghaw --- apps/issuance/src/issuance.service.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/issuance/src/issuance.service.ts b/apps/issuance/src/issuance.service.ts index 80c4cae46..c8891e31e 100644 --- a/apps/issuance/src/issuance.service.ts +++ b/apps/issuance/src/issuance.service.ts @@ -707,8 +707,10 @@ export class IssuanceService { respFileUpload = await this.issuanceRepository.saveFileUploadDetails(fileUpload); - + this.logger.log(`respFileUpload----${JSON.stringify(respFileUpload)}`); await parsedData.forEach(async (element, index) => { + this.logger.log(`element----${JSON.stringify(index)}`); + this.logger.log(`element----${JSON.stringify(element)}`); this.bulkIssuanceQueue.add( 'issue-credential', { From 6c884545ea87182cef7c204eb7a1f993fcaee28f Mon Sep 17 00:00:00 2001 From: tipusinghaw <126460794+tipusinghaw@users.noreply.github.com> Date: Fri, 10 Nov 2023 19:48:23 +0530 Subject: [PATCH 19/62] fix: added logs for bulk-issuance (#257) * fix: file details API issue Signed-off-by: tipusinghaw * added logger to test issuance Signed-off-by: tipusinghaw * added logger to test issuance Signed-off-by: tipusinghaw --------- Signed-off-by: tipusinghaw --- apps/issuance/src/issuance.service.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/issuance/src/issuance.service.ts b/apps/issuance/src/issuance.service.ts index 80c4cae46..c8891e31e 100644 --- a/apps/issuance/src/issuance.service.ts +++ b/apps/issuance/src/issuance.service.ts @@ -707,8 +707,10 @@ export class IssuanceService { respFileUpload = await this.issuanceRepository.saveFileUploadDetails(fileUpload); - + this.logger.log(`respFileUpload----${JSON.stringify(respFileUpload)}`); await parsedData.forEach(async (element, index) => { + this.logger.log(`element----${JSON.stringify(index)}`); + this.logger.log(`element----${JSON.stringify(element)}`); this.bulkIssuanceQueue.add( 'issue-credential', { From 7008c96be7c7301bb16363c25d8512df3c64e419 Mon Sep 17 00:00:00 2001 From: pallavighule <61926403+pallavicoder@users.noreply.github.com> Date: Mon, 13 Nov 2023 18:51:55 +0530 Subject: [PATCH 20/62] fix:configured helmet for X-Xss-Protection (#259) Signed-off-by: pallavicoder --- apps/api-gateway/src/main.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/api-gateway/src/main.ts b/apps/api-gateway/src/main.ts index e26eba808..86a012a15 100644 --- a/apps/api-gateway/src/main.ts +++ b/apps/api-gateway/src/main.ts @@ -18,7 +18,9 @@ async function bootstrap(): Promise { expressApp.set('x-powered-by', false); app.use(express.json({ limit: '50mb' })); app.use(express.urlencoded({ limit: '50mb' })); - app.use(helmet()); + app.use(helmet({ + xssFilter:true + })); app.useGlobalPipes(new ValidationPipe()); const options = new DocumentBuilder() .setTitle(`${process.env.PLATFORM_NAME}`) From ca704df0966485a273270930f677b807ca1e1e45 Mon Sep 17 00:00:00 2001 From: pallavighule <61926403+pallavicoder@users.noreply.github.com> Date: Mon, 13 Nov 2023 18:54:20 +0530 Subject: [PATCH 21/62] Fix:Added SQL Injection Mitigation Techniques (#249) * Fix:Added SQL Injection Mitigation Techniques Signed-off-by: pallavicoder * fix: added SQL injection mitigation technique Signed-off-by: pallavicoder * refact:removed await from function Signed-off-by: pallavicoder --------- Signed-off-by: pallavicoder --- apps/user/src/user.service.ts | 60 +++++++++++++++++++++-------------- package-lock.json | 1 + package.json | 1 + 3 files changed, 38 insertions(+), 24 deletions(-) diff --git a/apps/user/src/user.service.ts b/apps/user/src/user.service.ts index 3741681f8..acb124aac 100644 --- a/apps/user/src/user.service.ts +++ b/apps/user/src/user.service.ts @@ -41,6 +41,7 @@ import { SupabaseService } from '@credebl/supabase'; import { UserDevicesRepository } from '../repositories/user-device.repository'; import { v4 as uuidv4 } from 'uuid'; import { EcosystemConfigSettings } from '@credebl/enum/enum'; +import validator from 'validator'; import { DISALLOWED_EMAIL_DOMAIN } from '@credebl/common/common.constant'; @Injectable() export class UserService { @@ -286,6 +287,13 @@ export class UserService { } } + + private validateEmail(email: string): void { + if (!validator.isEmail(email)) { + throw new UnauthorizedException(ResponseMessages.user.error.invalidEmail); + } + } + /** * * @param loginUserDto @@ -293,32 +301,36 @@ export class UserService { */ async login(loginUserDto: LoginUserDto): Promise { const { email, password, isPasskey } = loginUserDto; - try { - const userData = await this.userRepository.checkUserExist(email); - if (!userData) { - throw new NotFoundException(ResponseMessages.user.error.notFound); - } - - if (userData && !userData.isEmailVerified) { - throw new BadRequestException(ResponseMessages.user.error.verifyMail); - } - - if (true === isPasskey && false === userData?.isFidoVerified) { - throw new UnauthorizedException(ResponseMessages.user.error.registerFido); - } - if (true === isPasskey && userData?.username && true === userData?.isFidoVerified) { - const getUserDetails = await this.userRepository.getUserDetails(userData.email); - const decryptedPassword = await this.commonService.decryptPassword(getUserDetails.password); - return this.generateToken(email, decryptedPassword); - } else { - const decryptedPassword = await this.commonService.decryptPassword(password); - return this.generateToken(email, decryptedPassword); + try { + this.validateEmail(email); + const userData = await this.userRepository.checkUserExist(email); + if (!userData) { + throw new NotFoundException(ResponseMessages.user.error.notFound); + } + + if (userData && !userData.isEmailVerified) { + throw new BadRequestException(ResponseMessages.user.error.verifyMail); + } + + if (true === isPasskey && false === userData?.isFidoVerified) { + throw new UnauthorizedException(ResponseMessages.user.error.registerFido); + } + + if (true === isPasskey && userData?.username && true === userData?.isFidoVerified) { + const getUserDetails = await this.userRepository.getUserDetails(userData.email); + const decryptedPassword = await this.commonService.decryptPassword(getUserDetails.password); + return this.generateToken(email, decryptedPassword); + } else { + const decryptedPassword = await this.commonService.decryptPassword(password); + return this.generateToken(email, decryptedPassword); + } + } catch (error) { + this.logger.error(`In Login User : ${JSON.stringify(error)}`); + throw new RpcException(error.response ? error.response : error); } - } catch (error) { - this.logger.error(`In Login User : ${JSON.stringify(error)}`); - throw new RpcException(error.response ? error.response : error); - } + + } async generateToken(email: string, password: string): Promise { diff --git a/package-lock.json b/package-lock.json index e78dbdf9d..970155b11 100644 --- a/package-lock.json +++ b/package-lock.json @@ -80,6 +80,7 @@ "typeorm": "^0.3.10", "unzipper": "^0.10.14", "uuid": "^9.0.0", + "validator": "^13.11.0", "web-push": "^3.6.4", "xml-js": "^1.6.11" }, diff --git a/package.json b/package.json index 40bf3f152..94d99e88f 100755 --- a/package.json +++ b/package.json @@ -97,6 +97,7 @@ "typeorm": "^0.3.10", "unzipper": "^0.10.14", "uuid": "^9.0.0", + "validator": "^13.11.0", "web-push": "^3.6.4", "xml-js": "^1.6.11" }, From eb78b699fc7b42dcd5c9780e64028cfeecf6c400 Mon Sep 17 00:00:00 2001 From: pallavighule <61926403+pallavicoder@users.noreply.github.com> Date: Wed, 15 Nov 2023 10:48:16 +0530 Subject: [PATCH 22/62] fix:modified dtos class validators messages (#260) Signed-off-by: pallavicoder --- apps/api-gateway/src/user/dto/add-user.dto.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/apps/api-gateway/src/user/dto/add-user.dto.ts b/apps/api-gateway/src/user/dto/add-user.dto.ts index e0dea93df..4007d8629 100644 --- a/apps/api-gateway/src/user/dto/add-user.dto.ts +++ b/apps/api-gateway/src/user/dto/add-user.dto.ts @@ -6,19 +6,19 @@ import { IsBoolean, IsEmail, IsNotEmpty, IsOptional, IsString } from 'class-vali export class AddUserDetails { @ApiProperty({ example: 'awqx@getnada.com' }) - @IsEmail() - @IsNotEmpty({ message: 'Please provide valid email' }) - @IsString({ message: 'email should be string' }) + @IsEmail({}, { message: 'Please provide a valid email' }) + @IsNotEmpty({ message: 'Email is required' }) + @IsString({ message: 'Email should be a string' }) email: string; @ApiProperty({ example: 'Alen' }) - @IsNotEmpty({ message: 'Please provide valid email' }) - @IsString({ message: 'firstName should be string' }) + @IsNotEmpty({ message: 'First name is required' }) + @IsString({ message: 'First name should be a string' }) firstName: string; @ApiProperty({ example: 'Harvey' }) - @IsNotEmpty({ message: 'Please provide valid email' }) - @IsString({ message: 'lastName should be string' }) + @IsNotEmpty({ message: 'Last name is required' }) + @IsString({ message: 'Last name should be a string' }) lastName: string; @ApiProperty() From 1548f4546819fd34b6333ae4bbd075dbf06aa505 Mon Sep 17 00:00:00 2001 From: tipusinghaw Date: Wed, 15 Nov 2023 15:46:35 +0530 Subject: [PATCH 23/62] fix: added bull module config in issuance service Signed-off-by: tipusinghaw --- apps/api-gateway/src/app.module.ts | 6 ++++++ apps/issuance/src/issuance.module.ts | 11 ++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/apps/api-gateway/src/app.module.ts b/apps/api-gateway/src/app.module.ts index 56f21423f..00b907bbf 100644 --- a/apps/api-gateway/src/app.module.ts +++ b/apps/api-gateway/src/app.module.ts @@ -49,6 +49,12 @@ import { BullModule } from '@nestjs/bull'; host: process.env.REDIS_HOST, port: parseInt(process.env.REDIS_PORT) } + }), + BullModule.registerQueue({ + name: 'bulk-issuance', + redis: { + port: parseInt(process.env.REDIS_PORT) + } }) ], controllers: [AppController], diff --git a/apps/issuance/src/issuance.module.ts b/apps/issuance/src/issuance.module.ts index 6201a237f..1473dd8b7 100644 --- a/apps/issuance/src/issuance.module.ts +++ b/apps/issuance/src/issuance.module.ts @@ -28,8 +28,17 @@ import { AwsService } from '@credebl/aws'; ]), CommonModule, CacheModule.register({ store: redisStore, host: process.env.REDIS_HOST, port: process.env.REDIS_PORT }), + BullModule.forRoot({ + redis: { + host: process.env.REDIS_HOST, + port: parseInt(process.env.REDIS_PORT) + } + }), BullModule.registerQueue({ - name: 'bulk-issuance' + name: 'bulk-issuance', + redis: { + port: parseInt(process.env.REDIS_PORT) + } }) ], controllers: [IssuanceController], From 7a0b073b23d396dbcea226de24cc37026f1da5ce Mon Sep 17 00:00:00 2001 From: tipusinghaw <126460794+tipusinghaw@users.noreply.github.com> Date: Wed, 15 Nov 2023 15:51:08 +0530 Subject: [PATCH 24/62] fix: added bull module config on issuance module (#261) * fix: file details API issue Signed-off-by: tipusinghaw * added logger to test issuance Signed-off-by: tipusinghaw * added logger to test issuance Signed-off-by: tipusinghaw * fix: added bull module config in issuance service Signed-off-by: tipusinghaw --------- Signed-off-by: tipusinghaw --- apps/api-gateway/src/app.module.ts | 6 ++++++ apps/issuance/src/issuance.module.ts | 11 ++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/apps/api-gateway/src/app.module.ts b/apps/api-gateway/src/app.module.ts index 56f21423f..00b907bbf 100644 --- a/apps/api-gateway/src/app.module.ts +++ b/apps/api-gateway/src/app.module.ts @@ -49,6 +49,12 @@ import { BullModule } from '@nestjs/bull'; host: process.env.REDIS_HOST, port: parseInt(process.env.REDIS_PORT) } + }), + BullModule.registerQueue({ + name: 'bulk-issuance', + redis: { + port: parseInt(process.env.REDIS_PORT) + } }) ], controllers: [AppController], diff --git a/apps/issuance/src/issuance.module.ts b/apps/issuance/src/issuance.module.ts index 6201a237f..1473dd8b7 100644 --- a/apps/issuance/src/issuance.module.ts +++ b/apps/issuance/src/issuance.module.ts @@ -28,8 +28,17 @@ import { AwsService } from '@credebl/aws'; ]), CommonModule, CacheModule.register({ store: redisStore, host: process.env.REDIS_HOST, port: process.env.REDIS_PORT }), + BullModule.forRoot({ + redis: { + host: process.env.REDIS_HOST, + port: parseInt(process.env.REDIS_PORT) + } + }), BullModule.registerQueue({ - name: 'bulk-issuance' + name: 'bulk-issuance', + redis: { + port: parseInt(process.env.REDIS_PORT) + } }) ], controllers: [IssuanceController], From eae2b7ccc0b33d4988be925875aee7ffa574aa00 Mon Sep 17 00:00:00 2001 From: tipusinghaw Date: Wed, 15 Nov 2023 16:32:04 +0530 Subject: [PATCH 25/62] fix: added bull module config in issuance service Signed-off-by: tipusinghaw --- apps/api-gateway/src/app.module.ts | 3 +++ apps/issuance/src/issuance.service.ts | 32 +++++++++++++++------------ 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/apps/api-gateway/src/app.module.ts b/apps/api-gateway/src/app.module.ts index 00b907bbf..48ca89ea5 100644 --- a/apps/api-gateway/src/app.module.ts +++ b/apps/api-gateway/src/app.module.ts @@ -21,6 +21,8 @@ import { UserModule } from './user/user.module'; import { ConnectionModule } from './connection/connection.module'; import { EcosystemModule } from './ecosystem/ecosystem.module'; import { BullModule } from '@nestjs/bull'; +import { CacheModule } from '@nestjs/cache-manager'; +import * as redisStore from 'cache-manager-redis-store'; @Module({ imports: [ @@ -44,6 +46,7 @@ import { BullModule } from '@nestjs/bull'; ConnectionModule, IssuanceModule, EcosystemModule, + CacheModule.register({ store: redisStore, host: process.env.REDIS_HOST, port: process.env.REDIS_PORT }), BullModule.forRoot({ redis: { host: process.env.REDIS_HOST, diff --git a/apps/issuance/src/issuance.service.ts b/apps/issuance/src/issuance.service.ts index c8891e31e..cb209020e 100644 --- a/apps/issuance/src/issuance.service.ts +++ b/apps/issuance/src/issuance.service.ts @@ -709,21 +709,25 @@ export class IssuanceService { this.logger.log(`respFileUpload----${JSON.stringify(respFileUpload)}`); await parsedData.forEach(async (element, index) => { - this.logger.log(`element----${JSON.stringify(index)}`); this.logger.log(`element----${JSON.stringify(element)}`); - this.bulkIssuanceQueue.add( - 'issue-credential', - { - data: element, - fileUploadId: respFileUpload.id, - cacheId: requestId, - credentialDefinitionId: parsedPrimeDetails.credentialDefinitionId, - schemaLedgerId: parsedPrimeDetails.schemaLedgerId, - orgId, - isLastData: index === parsedData.length - 1 - }, - { delay: 5000 } - ); + try { + this.bulkIssuanceQueue.add( + 'issue-credential', + { + data: element, + fileUploadId: respFileUpload.id, + cacheId: requestId, + credentialDefinitionId: parsedPrimeDetails.credentialDefinitionId, + schemaLedgerId: parsedPrimeDetails.schemaLedgerId, + orgId, + isLastData: index === parsedData.length - 1 + }, + { delay: 5000 } + ); + } catch (error) { + this.logger.error('Error adding item to the queue:', error); + } + }); return 'Process initiated for bulk issuance'; From 7ea0476e63ed282511af26cb2a1ad817df495a85 Mon Sep 17 00:00:00 2001 From: tipusinghaw Date: Wed, 15 Nov 2023 17:17:00 +0530 Subject: [PATCH 26/62] fix: added return type for processor functions Signed-off-by: tipusinghaw --- apps/issuance/src/issuance.processor.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/apps/issuance/src/issuance.processor.ts b/apps/issuance/src/issuance.processor.ts index 772bed2e6..ebd512e70 100644 --- a/apps/issuance/src/issuance.processor.ts +++ b/apps/issuance/src/issuance.processor.ts @@ -9,16 +9,14 @@ export class BulkIssuanceProcessor { constructor(private readonly issuanceService: IssuanceService) {} @OnQueueActive() - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/explicit-function-return-type - onActive(job: Job) { + onActive(job: Job): void { this.logger.log( `Processing job ${job.id} of type ${job.name} with data ${JSON.stringify(job.data)}...` ); } @Process('issue-credential') - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/explicit-function-return-type - async issueCredential(job: Job) { + async issueCredential(job: Job):Promise { this.logger.log( `Processing job ${job.id} of type ${job.name} with data ${JSON.stringify(job.data)}...` ); From de8a02710a7f3e9b4743a5600e58ffc689318cd6 Mon Sep 17 00:00:00 2001 From: Nishad Shirsat <103021375+nishad-ayanworks@users.noreply.github.com> Date: Thu, 16 Nov 2023 17:25:48 +0530 Subject: [PATCH 27/62] refactor: bulk issuance file data model (#264) * refactored file data model for status Signed-off-by: Nishad * refactored file data model for credentials Signed-off-by: Nishad --------- Signed-off-by: Nishad --- .../migrations/20231116112146_status_file_data/migration.sql | 4 ++++ .../20231116114452_credentials_file_data/migration.sql | 2 ++ libs/prisma-service/prisma/schema.prisma | 4 ++++ 3 files changed, 10 insertions(+) create mode 100644 libs/prisma-service/prisma/migrations/20231116112146_status_file_data/migration.sql create mode 100644 libs/prisma-service/prisma/migrations/20231116114452_credentials_file_data/migration.sql diff --git a/libs/prisma-service/prisma/migrations/20231116112146_status_file_data/migration.sql b/libs/prisma-service/prisma/migrations/20231116112146_status_file_data/migration.sql new file mode 100644 index 000000000..164da6ee3 --- /dev/null +++ b/libs/prisma-service/prisma/migrations/20231116112146_status_file_data/migration.sql @@ -0,0 +1,4 @@ +-- AlterTable +ALTER TABLE "file_data" ADD COLUMN "credDefId" TEXT, +ADD COLUMN "schemaId" TEXT, +ADD COLUMN "status" BOOLEAN NOT NULL DEFAULT false; diff --git a/libs/prisma-service/prisma/migrations/20231116114452_credentials_file_data/migration.sql b/libs/prisma-service/prisma/migrations/20231116114452_credentials_file_data/migration.sql new file mode 100644 index 000000000..48452a863 --- /dev/null +++ b/libs/prisma-service/prisma/migrations/20231116114452_credentials_file_data/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "file_data" ADD COLUMN "credential_data" JSONB; diff --git a/libs/prisma-service/prisma/schema.prisma b/libs/prisma-service/prisma/schema.prisma index fcef6ebce..84bee79d9 100644 --- a/libs/prisma-service/prisma/schema.prisma +++ b/libs/prisma-service/prisma/schema.prisma @@ -442,4 +442,8 @@ model file_data { deletedAt DateTime? @db.Timestamp(6) fileUploadId String fileUpload file_upload @relation(fields: [fileUploadId], references: [id]) + schemaId String? + credDefId String? + status Boolean @default(false) + credential_data Json? } From 6a064da8b16deb02f1e16b84021e396377c6d866 Mon Sep 17 00:00:00 2001 From: tipusinghaw Date: Thu, 16 Nov 2023 19:31:52 +0530 Subject: [PATCH 28/62] feat: implemented loop logic for bulk-issuance Signed-off-by: tipusinghaw --- apps/api-gateway/src/app.module.ts | 6 --- .../src/issuance/issuance.controller.ts | 2 +- .../interfaces/issuance.interfaces.ts | 2 +- apps/issuance/src/issuance.module.ts | 11 +---- apps/issuance/src/issuance.processor.ts | 2 +- apps/issuance/src/issuance.repository.ts | 37 +++++++++++----- apps/issuance/src/issuance.service.ts | 44 +++++++++---------- 7 files changed, 53 insertions(+), 51 deletions(-) diff --git a/apps/api-gateway/src/app.module.ts b/apps/api-gateway/src/app.module.ts index 48ca89ea5..9eee2c533 100644 --- a/apps/api-gateway/src/app.module.ts +++ b/apps/api-gateway/src/app.module.ts @@ -52,12 +52,6 @@ import * as redisStore from 'cache-manager-redis-store'; host: process.env.REDIS_HOST, port: parseInt(process.env.REDIS_PORT) } - }), - BullModule.registerQueue({ - name: 'bulk-issuance', - redis: { - port: parseInt(process.env.REDIS_PORT) - } }) ], controllers: [AppController], diff --git a/apps/api-gateway/src/issuance/issuance.controller.ts b/apps/api-gateway/src/issuance/issuance.controller.ts index ffd50a4bc..377fc6e7c 100644 --- a/apps/api-gateway/src/issuance/issuance.controller.ts +++ b/apps/api-gateway/src/issuance/issuance.controller.ts @@ -245,7 +245,7 @@ export class IssuanceController { const reqPayload: RequestPayload = { credDefId: credentialDefinitionId, fileKey, - fileName:file?.filename + fileName:file?.originalname }; this.logger.log(`reqPayload::::::${JSON.stringify(reqPayload)}`); const importCsvDetails = await this.issueCredentialService.importCsv( diff --git a/apps/issuance/interfaces/issuance.interfaces.ts b/apps/issuance/interfaces/issuance.interfaces.ts index 5941bc10b..b260d7603 100644 --- a/apps/issuance/interfaces/issuance.interfaces.ts +++ b/apps/issuance/interfaces/issuance.interfaces.ts @@ -87,7 +87,7 @@ export interface FileUpload { name?: string; upload_type?: string; status?: string; - orgId?: number | string; + orgId?: string; createDateTime?: Date; lastChangedDateTime?: Date; } diff --git a/apps/issuance/src/issuance.module.ts b/apps/issuance/src/issuance.module.ts index 1473dd8b7..6201a237f 100644 --- a/apps/issuance/src/issuance.module.ts +++ b/apps/issuance/src/issuance.module.ts @@ -28,17 +28,8 @@ import { AwsService } from '@credebl/aws'; ]), CommonModule, CacheModule.register({ store: redisStore, host: process.env.REDIS_HOST, port: process.env.REDIS_PORT }), - BullModule.forRoot({ - redis: { - host: process.env.REDIS_HOST, - port: parseInt(process.env.REDIS_PORT) - } - }), BullModule.registerQueue({ - name: 'bulk-issuance', - redis: { - port: parseInt(process.env.REDIS_PORT) - } + name: 'bulk-issuance' }) ], controllers: [IssuanceController], diff --git a/apps/issuance/src/issuance.processor.ts b/apps/issuance/src/issuance.processor.ts index ebd512e70..93c7487cf 100644 --- a/apps/issuance/src/issuance.processor.ts +++ b/apps/issuance/src/issuance.processor.ts @@ -21,6 +21,6 @@ export class BulkIssuanceProcessor { `Processing job ${job.id} of type ${job.name} with data ${JSON.stringify(job.data)}...` ); - this.issuanceService.processIssuanceData(job.data); + await this.issuanceService.processIssuanceData(job.data); } } diff --git a/apps/issuance/src/issuance.repository.ts b/apps/issuance/src/issuance.repository.ts index cf8da558c..c0b03ceff 100644 --- a/apps/issuance/src/issuance.repository.ts +++ b/apps/issuance/src/issuance.repository.ts @@ -4,7 +4,7 @@ import { PrismaService } from '@credebl/prisma-service'; // eslint-disable-next-line camelcase import { agent_invitations, credentials, file_data, file_upload, org_agents, organisation, platform_config, shortening_url } from '@prisma/client'; import { ResponseMessages } from '@credebl/common/response-messages'; -import { FileUpload, FileUploadData, PreviewRequest, SchemaDetails } from '../interfaces/issuance.interfaces'; +import { FileUploadData, PreviewRequest, SchemaDetails } from '../interfaces/issuance.interfaces'; @Injectable() export class IssuanceRepository { @@ -198,13 +198,13 @@ export class IssuanceRepository { } } - async saveFileUploadDetails(fileUploadPayload: FileUpload): Promise { + async saveFileUploadDetails(fileUploadPayload): Promise { try { - const { name, orgId, status, upload_type } = fileUploadPayload; + const { name, status, upload_type, orgId } = fileUploadPayload; return this.prisma.file_upload.create({ data: { name, - orgId: `${orgId}`, + orgId: String(orgId), status, upload_type } @@ -216,18 +216,15 @@ export class IssuanceRepository { } } - async updateFileUploadDetails(fileId: string, fileUploadPayload: FileUpload): Promise { + async updateFileUploadDetails(fileId: string, fileUploadPayload): Promise { try { - const { name, orgId, status, upload_type } = fileUploadPayload; + const { status } = fileUploadPayload; return this.prisma.file_upload.update({ where: { id: fileId }, data: { - name, - orgId: `${orgId}`, - status, - upload_type + status } }); @@ -337,4 +334,24 @@ export class IssuanceRepository { throw error; } } + + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/explicit-function-return-type, @typescript-eslint/no-unused-vars + async saveFileDetails(fileData) { + try { + // const { fileUpload, isError, referenceId, error, detailError } = fileData; + // return this.prisma.file_data.create({ + // data: { + // detailError, + // error, + // isError, + // referenceId, + // fileUploadId: fileUpload + // } + // }); + + } catch (error) { + this.logger.error(`[saveFileUploadData] - error: ${JSON.stringify(error)}`); + throw error; + } + } } \ No newline at end of file diff --git a/apps/issuance/src/issuance.service.ts b/apps/issuance/src/issuance.service.ts index cb209020e..bcc049ac1 100644 --- a/apps/issuance/src/issuance.service.ts +++ b/apps/issuance/src/issuance.service.ts @@ -695,10 +695,9 @@ export class IssuanceService { const parsedData = JSON.parse(cachedData as string).fileData.data; const parsedPrimeDetails = JSON.parse(cachedData as string); - fileUpload.upload_type = FileUploadType.Issuance; fileUpload.status = FileUploadStatus.started; - fileUpload.orgId = orgId; + fileUpload.orgId = String(orgId); fileUpload.createDateTime = new Date(); if (parsedPrimeDetails && parsedPrimeDetails.fileName) { @@ -707,29 +706,30 @@ export class IssuanceService { respFileUpload = await this.issuanceRepository.saveFileUploadDetails(fileUpload); - this.logger.log(`respFileUpload----${JSON.stringify(respFileUpload)}`); - await parsedData.forEach(async (element, index) => { - this.logger.log(`element----${JSON.stringify(element)}`); - try { - this.bulkIssuanceQueue.add( - 'issue-credential', - { - data: element, - fileUploadId: respFileUpload.id, - cacheId: requestId, - credentialDefinitionId: parsedPrimeDetails.credentialDefinitionId, - schemaLedgerId: parsedPrimeDetails.schemaLedgerId, - orgId, - isLastData: index === parsedData.length - 1 - }, - { delay: 5000 } - ); - } catch (error) { - this.logger.error('Error adding item to the queue:', error); - } + + await parsedData.forEach(async (element) => { + await this.issuanceRepository.saveFileDetails(element); }); + // this.logger.log(`respFileUpload----${JSON.stringify(respFileUpload)}`); + // await parsedData.forEach(async (element, index) => { + // this.logger.log(`element11----${JSON.stringify(element)}`); + // const payload = + // { + // data: element, + // fileUploadId: respFileUpload.id, + // cacheId: requestId, + // credentialDefinitionId: parsedPrimeDetails.credentialDefinitionId, + // schemaLedgerId: parsedPrimeDetails.schemaLedgerId, + // orgId, + // isLastData: index === parsedData.length - 1 + // }; + + // this.processIssuanceData(payload); + + // }); + return 'Process initiated for bulk issuance'; } catch (error) { fileUpload.status = FileUploadStatus.interrupted; From 26f9f8d05f959b831316d21f8bf63561cda4024d Mon Sep 17 00:00:00 2001 From: bhavanakarwade Date: Thu, 16 Nov 2023 20:05:59 +0530 Subject: [PATCH 29/62] resolved eslint errors Signed-off-by: bhavanakarwade --- apps/api-gateway/src/user/user.controller.ts | 30 +++++++++----- apps/api-gateway/src/user/user.service.ts | 8 +++- apps/user/repositories/user.repository.ts | 39 +++++++++++++++--- apps/user/src/fido/fido.module.ts | 2 + apps/user/src/user.controller.ts | 6 +++ apps/user/src/user.module.ts | 3 ++ apps/user/src/user.service.ts | 33 ++++++++++++--- libs/aws/src/aws.service.ts | 42 ++++++++------------ libs/common/src/response-messages/index.ts | 1 + 9 files changed, 115 insertions(+), 49 deletions(-) diff --git a/apps/api-gateway/src/user/user.controller.ts b/apps/api-gateway/src/user/user.controller.ts index 65bb13c23..16941a59b 100644 --- a/apps/api-gateway/src/user/user.controller.ts +++ b/apps/api-gateway/src/user/user.controller.ts @@ -10,8 +10,7 @@ import { BadRequestException, Get, Query, - UseGuards, - UploadedFile + UseGuards } from '@nestjs/common'; import { UserService } from './user.service'; import { @@ -258,6 +257,21 @@ export class UserController { return res.status(HttpStatus.OK).json(finalResponse); } + @Get('/user-credentials/:id') + @ApiOperation({ summary: 'Get user credentials by Id', description: 'Get user credentials by Id' }) + @ApiResponse({ status: 200, description: 'Success', type: ApiResponseDto }) + async getUserCredentialsById (@Param('id') id: string, @Res() res: Response): Promise { + const getUserCrdentialsById = await this.userService.getUserCredentialsById(id); + + const finalResponse: IResponseType = { + statusCode: HttpStatus.OK, + message: ResponseMessages.user.success.userCredentials, + data: getUserCrdentialsById.response + }; + return res.status(HttpStatus.OK).json(finalResponse); + } + + /** * * @param acceptRejectInvitation @@ -296,20 +310,16 @@ export class UserController { @ApiResponse({ status: 200, description: 'Success', type: ApiResponseDto }) async shareUserCertificate( @Body() shareUserCredentials: CreateUserCertificateDto, - @UploadedFile() file: Express.Multer.File, @Res() res: Response - ): Promise { - const imageBuffer = await this.userService.shareUserCertificate(shareUserCredentials); - if (file) { - const certificateImageBuffer = imageBuffer.response; - const imageUrl = await this.awsService.uploads3(certificateImageBuffer, 'png', './certificates', 'base64'); + ): Promise { + const imageBuffer = await this.userService.shareUserCertificate(shareUserCredentials); + const finalResponse: IResponseType = { statusCode: HttpStatus.CREATED, message: 'Certificate url generated successfully', - data: imageUrl + data: imageBuffer.response }; return res.status(HttpStatus.CREATED).json(finalResponse); - } } @Put('/') diff --git a/apps/api-gateway/src/user/user.service.ts b/apps/api-gateway/src/user/user.service.ts index 52a5f9394..9b682a3e8 100644 --- a/apps/api-gateway/src/user/user.service.ts +++ b/apps/api-gateway/src/user/user.service.ts @@ -26,6 +26,12 @@ export class UserService extends BaseService { return this.sendNats(this.serviceProxy, 'get-user-public-profile', payload); } + + async getUserCredentialsById(id: string): Promise<{ response: object }> { + const payload = { id }; + return this.sendNats(this.serviceProxy, 'get-user-credentials-by-id', payload); + } + async updateUserProfile(updateUserProfileDto: UpdateUserProfileDto): Promise<{ response: object }> { const payload = { updateUserProfileDto }; return this.sendNats(this.serviceProxy, 'update-user-profile', payload); @@ -57,7 +63,7 @@ export class UserService extends BaseService { const payload = { shareUserCredentials}; return this.sendNats(this.serviceProxy, 'share-user-certificate', payload); } - + async get( getAllUsersDto: GetAllUsersDto ): Promise<{ response: object }> { diff --git a/apps/user/repositories/user.repository.ts b/apps/user/repositories/user.repository.ts index 484f73bff..c407062c2 100644 --- a/apps/user/repositories/user.repository.ts +++ b/apps/user/repositories/user.repository.ts @@ -103,6 +103,19 @@ export class UserRepository { return this.findUser(queryOptions); } + /** + * + * @param id + * @returns User profile data + */ + async getUserCredentialsById(id: string): Promise { + return this.prisma.user_credentials.findUnique({ + where: { + id + } + }); + } + /** * * @param id @@ -115,6 +128,7 @@ export class UserRepository { return this.findUserForPublicProfile(queryOptions); } + /** * @@ -441,12 +455,11 @@ export class UserRepository { async getAttributesBySchemaId(shareUserCertificate: ShareUserCertificateI): Promise { try { - const getAttributes = await this.prisma.schema - .findFirst({ - where: { - schemaLedgerId: shareUserCertificate.schemaId - } - }); + const getAttributes = await this.prisma.schema.findFirst({ + where: { + schemaLedgerId: shareUserCertificate.schemaId + } + }); return getAttributes; } catch (error) { this.logger.error(`checkSchemaExist:${JSON.stringify(error)}`); @@ -454,6 +467,20 @@ export class UserRepository { } } + async saveCertificateImageUrl(imageUrl: string, credentialId: string): Promise { + try { + const saveImageUrl = await this.prisma.user_credentials.create({ + data: { + imageUrl, + credentialId + } + }); + return saveImageUrl; + } catch (error) { + throw new Error(`Error saving certificate image URL: ${error.message}`); + } + } + async checkUniqueUserExist(email: string): Promise { try { return this.prisma.user.findUnique({ diff --git a/apps/user/src/fido/fido.module.ts b/apps/user/src/fido/fido.module.ts index a5106d814..14077a4b3 100644 --- a/apps/user/src/fido/fido.module.ts +++ b/apps/user/src/fido/fido.module.ts @@ -19,6 +19,7 @@ import { UserOrgRolesRepository } from 'libs/user-org-roles/repositories'; import { UserOrgRolesService } from '@credebl/user-org-roles'; import { UserRepository } from '../../repositories/user.repository'; import { UserService } from '../user.service'; +import { AwsService } from '@credebl/aws'; @Module({ imports: [ @@ -36,6 +37,7 @@ import { UserService } from '../user.service'; ], controllers: [FidoController], providers: [ + AwsService, UserService, PrismaService, FidoService, diff --git a/apps/user/src/user.controller.ts b/apps/user/src/user.controller.ts index e17b18aab..e21c85a73 100644 --- a/apps/user/src/user.controller.ts +++ b/apps/user/src/user.controller.ts @@ -61,6 +61,12 @@ export class UserController { return this.userService.findUserByEmail(payload); } + + @MessagePattern({ cmd: 'get-user-credentials-by-id' }) + async getUserCredentialsById(payload: { id }): Promise { + return this.userService.getUserCredentialsById(payload); + } + @MessagePattern({ cmd: 'get-org-invitations' }) async invitations(payload: { id; status; pageNumber; pageSize; search; }): Promise { return this.userService.invitations(payload); diff --git a/apps/user/src/user.module.ts b/apps/user/src/user.module.ts index 865dc6e04..9ce90e213 100644 --- a/apps/user/src/user.module.ts +++ b/apps/user/src/user.module.ts @@ -17,6 +17,7 @@ import { UserOrgRolesService } from '@credebl/user-org-roles'; import { UserRepository } from '../repositories/user.repository'; import { UserService } from './user.service'; import { UserDevicesRepository } from '../repositories/user-device.repository'; +import { AwsService } from '@credebl/aws'; @Module({ imports: [ @@ -29,12 +30,14 @@ import { UserDevicesRepository } from '../repositories/user-device.repository'; } } ]), + CommonModule, FidoModule, OrgRolesModule ], controllers: [UserController], providers: [ + AwsService, UserService, UserRepository, PrismaService, diff --git a/apps/user/src/user.service.ts b/apps/user/src/user.service.ts index 51c0242fe..83ff2cee8 100644 --- a/apps/user/src/user.service.ts +++ b/apps/user/src/user.service.ts @@ -49,6 +49,8 @@ import { ArbiterTemplate } from '../templates/arbiter-template'; import * as puppeteer from 'puppeteer'; import validator from 'validator'; import { DISALLOWED_EMAIL_DOMAIN } from '@credebl/common/common.constant'; +import { AwsService } from '@credebl/aws'; +import { readFileSync } from 'fs'; @Injectable() export class UserService { constructor( @@ -60,6 +62,7 @@ export class UserService { private readonly userOrgRoleService: UserOrgRolesService, private readonly userActivityService: UserActivityService, private readonly userRepository: UserRepository, + private readonly awsService: AwsService, private readonly userDevicesRepository: UserDevicesRepository, private readonly logger: Logger, @Inject('NATS_CLIENT') private readonly userServiceProxy: ClientProxy @@ -397,6 +400,16 @@ export class UserService { } } + async getUserCredentialsById(payload: { id }): Promise { + try { + const userCredentials = await this.userRepository.getUserCredentialsById(payload.id); + return userCredentials; + } catch (error) { + this.logger.error(`get user: ${JSON.stringify(error)}`); + throw new RpcException(error.response ? error.response : error); + } + } + async updateUserProfile(updateUserProfileDto: UpdateUserProfile): Promise { try { return this.userRepository.updateUserProfile(updateUserProfileDto); @@ -569,22 +582,30 @@ export class UserService { throw new NotFoundException('error in get attributes'); } - const imageBuffer = await this.convertHtmlToImage(template); - return imageBuffer; + const imageBuffer = await this.convertHtmlToImage(template, shareUserCertificate.credentialId); + const verifyCode = uuidv4(); + // const myFile = new File([readFileSync(imageBuffer)], `cert_${shareUserCertificate.credentialId}.jpeg`); + + const imageUrl = await this.awsService.uploads3(imageBuffer, 'jpeg', verifyCode, 'certificates', 'base64'); + + return this.saveCertificateUrl(imageUrl, shareUserCertificate.credentialId); + } + + async saveCertificateUrl(imageUrl: string, credentialId: string): Promise { + return this.userRepository.saveCertificateImageUrl(imageUrl, credentialId); } - async convertHtmlToImage(template: string): Promise { + async convertHtmlToImage(template: string, credentialId: string): Promise { const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.setContent(template); - const screenshot = await page.screenshot({ path: 'cert1.png' }); - + const filename = `cert_${credentialId}.jpeg`; + const screenshot = await page.screenshot({ path: filename }); await browser.close(); return screenshot; } - /** * * @param acceptRejectInvitation diff --git a/libs/aws/src/aws.service.ts b/libs/aws/src/aws.service.ts index 6b112c59a..45089b636 100644 --- a/libs/aws/src/aws.service.ts +++ b/libs/aws/src/aws.service.ts @@ -1,6 +1,7 @@ -import { BadRequestException, HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { RpcException } from '@nestjs/microservices'; import { S3 } from 'aws-sdk'; +import { promisify } from 'util'; @Injectable() export class AwsService { @@ -17,39 +18,28 @@ export class AwsService { async uploads3( fileBuffer: Buffer, ext: string, + verifyCode: string, pathAWS: string = '', encoding = 'base64', - filename = 'nftp' + filename: string = 'cerficate' ): Promise { const timestamp = Date.now(); - await this.s3.putObject( - { + const putObjectAsync = promisify(this.s3.putObject).bind(this.s3); + + try { + await putObjectAsync({ Bucket: process.env.AWS_PUBLIC_BUCKET_NAME, Key: `${pathAWS}/${encodeURIComponent(filename)}.${timestamp}.${ext}`, - Body: fileBuffer.toString(), - ContentEncoding: encoding - }, - (err) => { - if (err) { - throw new HttpException('An error occurred while uploading the image', HttpStatus.SERVICE_UNAVAILABLE); - } else { - return 'photo is uploaded'; - } - } - ); + Body: fileBuffer, + ContentEncoding: encoding, + ContentType: `image/jpeg` + }); - return `https://${process.env.AWS_PUBLIC_BUCKET_NAME}.s3.amazonaws.com/${pathAWS}/${encodeURIComponent( - filename - )}-${timestamp}.${ext}`; - } + // return `https://${process.env.AWS_PUBLIC_BUCKET_NAME}.s3.${process.env.AWS_PUBLIC_REGION}.amazonaws.com/${pathAWS}/${encodeURIComponent(filename)}.${timestamp}.${ext}`; + return `${process.env.FRONT_END_URL}/certificates/${verifyCode}/${encodeURIComponent(filename)}.${timestamp}.${ext}`; - async fileUpload(file: Express.Multer.File): Promise { - const fileExt = file['originalname'].split('.')[file['originalname'].split('.').length - 1]; - if ('image/png' === file['mimetype'] || 'image/jpg' === file['mimetype'] || 'image/jpeg' === file['mimetype']) { - const awsResponse = await this.uploads3(file['buffer'], fileExt, file['mimetype'], 'images'); - return awsResponse; - } else { - throw new BadRequestException('File format should be PNG,JPG,JPEG'); + } catch (err) { + throw new HttpException('An error occurred while uploading the image', HttpStatus.SERVICE_UNAVAILABLE); } } diff --git a/libs/common/src/response-messages/index.ts b/libs/common/src/response-messages/index.ts index 9c3c29bd5..501cd9aba 100644 --- a/libs/common/src/response-messages/index.ts +++ b/libs/common/src/response-messages/index.ts @@ -15,6 +15,7 @@ export const ResponseMessages = { checkEmail: 'User email checked successfully.', sendVerificationCode: 'Verification code has been sent sucessfully to the mail. Please verify', userActivity: 'User activities fetched successfully', + userCredentials: 'User credentials fetched successfully', platformEcosystemettings: 'Platform and ecosystem settings updated', fetchPlatformSettings: 'Platform settings fetched' }, From 75a90d81a78afe3afebe092c9290da3736622648 Mon Sep 17 00:00:00 2001 From: tipusinghaw Date: Fri, 17 Nov 2023 16:50:39 +0530 Subject: [PATCH 30/62] feat: implemented bulk-issuance using loop Signed-off-by: tipusinghaw --- apps/api-gateway/src/authz/socket.gateway.ts | 8 ++ .../src/issuance/dtos/issuance.dto.ts | 8 ++ .../src/issuance/interfaces/index.ts | 4 +- .../src/issuance/issuance.controller.ts | 10 +-- .../src/issuance/issuance.service.ts | 4 +- .../interfaces/issuance.interfaces.ts | 2 + apps/issuance/src/issuance.controller.ts | 4 +- apps/issuance/src/issuance.repository.ts | 63 +++++++++----- apps/issuance/src/issuance.service.ts | 83 +++++++++++++------ 9 files changed, 127 insertions(+), 59 deletions(-) diff --git a/apps/api-gateway/src/authz/socket.gateway.ts b/apps/api-gateway/src/authz/socket.gateway.ts index a8a9bc44b..7e46ffcea 100644 --- a/apps/api-gateway/src/authz/socket.gateway.ts +++ b/apps/api-gateway/src/authz/socket.gateway.ts @@ -102,4 +102,12 @@ export class SocketGateway implements OnGatewayConnection { .to(payload.clientId) .emit('error-in-wallet-creation-process', payload.error); } + + @SubscribeMessage('bulk-issuance-process-completed') + async handleBulkIssuance(payload: ISocketInterface): Promise { + this.logger.log(`bulk-issuance-process-completed ${payload.clientId}`); + this.server + .to(payload.clientId) + .emit('bulk-issuance-process-completed', payload.error); + } } diff --git a/apps/api-gateway/src/issuance/dtos/issuance.dto.ts b/apps/api-gateway/src/issuance/dtos/issuance.dto.ts index a024e3591..0785de5f6 100644 --- a/apps/api-gateway/src/issuance/dtos/issuance.dto.ts +++ b/apps/api-gateway/src/issuance/dtos/issuance.dto.ts @@ -205,4 +205,12 @@ export class FileParameter { @Type(() => String) sortValue = ''; +} + +export class ClientDetails { + @ApiProperty({ required: false }) + @IsOptional() + @Type(() => String) + clientId = ''; + } \ No newline at end of file diff --git a/apps/api-gateway/src/issuance/interfaces/index.ts b/apps/api-gateway/src/issuance/interfaces/index.ts index c252de37c..87aa925dd 100644 --- a/apps/api-gateway/src/issuance/interfaces/index.ts +++ b/apps/api-gateway/src/issuance/interfaces/index.ts @@ -59,11 +59,11 @@ export class IUserOrg { export interface FileExportResponse { response: unknown; fileContent: string; - fileName : string + fileName: string } export interface RequestPayload { credDefId: string; fileKey: string; fileName: string; - } +} diff --git a/apps/api-gateway/src/issuance/issuance.controller.ts b/apps/api-gateway/src/issuance/issuance.controller.ts index 377fc6e7c..70213e712 100644 --- a/apps/api-gateway/src/issuance/issuance.controller.ts +++ b/apps/api-gateway/src/issuance/issuance.controller.ts @@ -38,7 +38,7 @@ import { CommonService } from '@credebl/common/common.service'; import { Response } from 'express'; import IResponseType from '@credebl/common/interfaces/response.interface'; import { IssuanceService } from './issuance.service'; -import { FileParameter, IssuanceDto, IssueCredentialDto, OutOfBandCredentialDto, PreviewFileDetails } from './dtos/issuance.dto'; +import { ClientDetails, FileParameter, IssuanceDto, IssueCredentialDto, OutOfBandCredentialDto, PreviewFileDetails } from './dtos/issuance.dto'; import { IUserRequest } from '@credebl/user-request/user-request.interface'; import { User } from '../authz/decorators/user.decorator'; import { ResponseMessages } from '@credebl/common/response-messages'; @@ -245,7 +245,7 @@ export class IssuanceController { const reqPayload: RequestPayload = { credDefId: credentialDefinitionId, fileKey, - fileName:file?.originalname + fileName: file?.originalname }; this.logger.log(`reqPayload::::::${JSON.stringify(reqPayload)}`); const importCsvDetails = await this.issueCredentialService.importCsv( @@ -389,7 +389,7 @@ export class IssuanceController { }; return res.status(HttpStatus.OK).json(finalResponse); } - + @Get('/orgs/:orgId/:fileId/bulk/file-data') @Roles(OrgRoles.OWNER, OrgRoles.ADMIN, OrgRoles.ISSUER, OrgRoles.VERIFIER) @UseGuards(AuthGuard('jwt'), OrgRolesGuard) @@ -473,8 +473,8 @@ export class IssuanceController { summary: 'bulk issue credential', description: 'bulk issue credential' }) - async issueBulkCredentials(@Param('requestId') requestId: string, @Param('orgId') orgId: number, @Res() res: Response): Promise { - const bulkIssunaceDetails = await this.issueCredentialService.issueBulkCredential(requestId, orgId); + async issueBulkCredentials(@Param('requestId') requestId: string, @Param('orgId') orgId: number, @Res() res: Response, @Body() clientDetails: ClientDetails): Promise { + const bulkIssunaceDetails = await this.issueCredentialService.issueBulkCredential(requestId, orgId, clientDetails.clientId); const finalResponse: IResponseType = { statusCode: HttpStatus.CREATED, message: ResponseMessages.issuance.success.bulkIssuance, diff --git a/apps/api-gateway/src/issuance/issuance.service.ts b/apps/api-gateway/src/issuance/issuance.service.ts index 785292794..b79664a72 100644 --- a/apps/api-gateway/src/issuance/issuance.service.ts +++ b/apps/api-gateway/src/issuance/issuance.service.ts @@ -107,8 +107,8 @@ export class IssuanceService extends BaseService { return this.sendNats(this.issuanceProxy, 'issued-file-data', payload); } - async issueBulkCredential(requestId: string, orgId: number): Promise<{ response: object }> { - const payload = { requestId, orgId }; + async issueBulkCredential(requestId: string, orgId: number, clientId: string): Promise<{ response: object }> { + const payload = { requestId, orgId, clientId }; return this.sendNats(this.issuanceProxy, 'issue-bulk-credentials', payload); } } diff --git a/apps/issuance/interfaces/issuance.interfaces.ts b/apps/issuance/interfaces/issuance.interfaces.ts index b260d7603..a5f02ba08 100644 --- a/apps/issuance/interfaces/issuance.interfaces.ts +++ b/apps/issuance/interfaces/issuance.interfaces.ts @@ -100,4 +100,6 @@ export interface FileUploadData { createDateTime: Date; error?: string; detailError?: string; + jobId: string; + clientId: string; } \ No newline at end of file diff --git a/apps/issuance/src/issuance.controller.ts b/apps/issuance/src/issuance.controller.ts index 3fb0f1700..fb7fb54ab 100644 --- a/apps/issuance/src/issuance.controller.ts +++ b/apps/issuance/src/issuance.controller.ts @@ -84,7 +84,7 @@ export class IssuanceController { @MessagePattern({ cmd: 'issue-bulk-credentials' }) - async issueBulkCredentials(payload: { requestId: string, orgId: number }): Promise { - return this.issuanceService.issueBulkCredential(payload.requestId, payload.orgId); + async issueBulkCredentials(payload: { requestId: string, orgId: number, clientId: string }): Promise { + return this.issuanceService.issueBulkCredential(payload.requestId, payload.orgId, payload.clientId); } } diff --git a/apps/issuance/src/issuance.repository.ts b/apps/issuance/src/issuance.repository.ts index c0b03ceff..394935941 100644 --- a/apps/issuance/src/issuance.repository.ts +++ b/apps/issuance/src/issuance.repository.ts @@ -4,7 +4,7 @@ import { PrismaService } from '@credebl/prisma-service'; // eslint-disable-next-line camelcase import { agent_invitations, credentials, file_data, file_upload, org_agents, organisation, platform_config, shortening_url } from '@prisma/client'; import { ResponseMessages } from '@credebl/common/response-messages'; -import { FileUploadData, PreviewRequest, SchemaDetails } from '../interfaces/issuance.interfaces'; +import { FileUploadData, PreviewRequest, SchemaDetails } from '../interfaces/issuance.interfaces'; @Injectable() export class IssuanceRepository { @@ -316,16 +316,42 @@ export class IssuanceRepository { } - async saveFileUploadData(fileUploadData: FileUploadData): Promise { + async updateFileUploadData(fileUploadData: FileUploadData): Promise { try { - const { fileUpload, isError, referenceId, error, detailError } = fileUploadData; + const { jobId, fileUpload, isError, referenceId, error, detailError } = fileUploadData; + + if (jobId) { + return this.prisma.file_data.update({ + where: { id: jobId }, + data: { + detailError, + error, + isError, + referenceId, + fileUploadId: fileUpload + } + }); + } else { + throw error; + } + } catch (error) { + this.logger.error(`[saveFileUploadData] - error: ${JSON.stringify(error)}`); + throw error; + } + } + + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/explicit-function-return-type, @typescript-eslint/no-unused-vars + async saveFileDetails(fileData) { + try { + const { credential_data, schemaId, credDefId, status, isError, fileUploadId } = fileData; return this.prisma.file_data.create({ data: { - detailError, - error, - isError, - referenceId, - fileUploadId: fileUpload + credential_data, + schemaId, + credDefId, + status, + fileUploadId, + isError } }); @@ -335,22 +361,15 @@ export class IssuanceRepository { } } - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/explicit-function-return-type, @typescript-eslint/no-unused-vars - async saveFileDetails(fileData) { + async getFileDetails(fileId: string): Promise { try { - // const { fileUpload, isError, referenceId, error, detailError } = fileData; - // return this.prisma.file_data.create({ - // data: { - // detailError, - // error, - // isError, - // referenceId, - // fileUploadId: fileUpload - // } - // }); - + return this.prisma.file_data.findMany({ + where: { + fileUploadId: fileId + } + }); } catch (error) { - this.logger.error(`[saveFileUploadData] - error: ${JSON.stringify(error)}`); + this.logger.error(`[getFileDetails] - error: ${JSON.stringify(error)}`); throw error; } } diff --git a/apps/issuance/src/issuance.service.ts b/apps/issuance/src/issuance.service.ts index bcc049ac1..62fc6195d 100644 --- a/apps/issuance/src/issuance.service.ts +++ b/apps/issuance/src/issuance.service.ts @@ -26,6 +26,7 @@ import { InjectQueue } from '@nestjs/bull'; import { Queue } from 'bull'; import { FileUploadStatus, FileUploadType } from 'apps/api-gateway/src/enum'; import { AwsService } from '@credebl/aws'; +import { io } from 'socket.io-client'; @Injectable() export class IssuanceService { @@ -664,7 +665,7 @@ export class IssuanceService { } } - async issueBulkCredential(requestId: string, orgId: number): Promise { + async issueBulkCredential(requestId: string, orgId: number, clientId:string): Promise { const fileUpload: { lastChangedDateTime: Date; name?: string; @@ -685,11 +686,13 @@ export class IssuanceService { `Param 'requestId' is missing from the request.` ); } + this.logger.log(`requestId----${JSON.stringify(requestId)}`); + try { const cachedData = await this.cacheManager.get(requestId); this.logger.log(`cachedData----${JSON.stringify(cachedData)}`); - if (cachedData === undefined) { + if (!cachedData) { throw new BadRequestException(ResponseMessages.issuance.error.cacheTimeOut); } @@ -706,30 +709,44 @@ export class IssuanceService { respFileUpload = await this.issuanceRepository.saveFileUploadDetails(fileUpload); + const saveFileDetailsPromises = parsedData.map(async (element) => { + const credentialPayload = { + credential_data: element, + schemaId: parsedPrimeDetails.schemaLedgerId, + credDefId: parsedPrimeDetails.credentialDefinitionId, + state: false, + isError: false, + fileUploadId: respFileUpload.id + }; + return this.issuanceRepository.saveFileDetails(credentialPayload); + }); + + // Wait for all saveFileDetails operations to complete + await Promise.all(saveFileDetailsPromises); + + // Now fetch the file details + const respFile = await this.issuanceRepository.getFileDetails(respFileUpload.id); + if (!respFile) { + throw new BadRequestException('File data does not exist for the specific file'); + } + await respFile.forEach(async (element, index) => { + this.logger.log(`element11----${JSON.stringify(element)}`); + const payload = + { + data: element.credential_data, + fileUploadId: element.fileUploadId, + clientId, + cacheId: requestId, + credentialDefinitionId: element.credDefId, + schemaLedgerId: element.schemaId, + orgId, + id: element.id, + isLastData: index === respFile.length - 1 + }; + this.processIssuanceData(payload); - await parsedData.forEach(async (element) => { - - await this.issuanceRepository.saveFileDetails(element); }); - // this.logger.log(`respFileUpload----${JSON.stringify(respFileUpload)}`); - // await parsedData.forEach(async (element, index) => { - // this.logger.log(`element11----${JSON.stringify(element)}`); - // const payload = - // { - // data: element, - // fileUploadId: respFileUpload.id, - // cacheId: requestId, - // credentialDefinitionId: parsedPrimeDetails.credentialDefinitionId, - // schemaLedgerId: parsedPrimeDetails.schemaLedgerId, - // orgId, - // isLastData: index === parsedData.length - 1 - // }; - - // this.processIssuanceData(payload); - - // }); - return 'Process initiated for bulk issuance'; } catch (error) { fileUpload.status = FileUploadStatus.interrupted; @@ -754,7 +771,9 @@ export class IssuanceService { referenceId: '', createDateTime: undefined, error: '', - detailError: '' + detailError: '', + jobId: '', + clientId: '' }; this.logger.log(`jobDetails----${JSON.stringify(jobDetails)}`); @@ -763,6 +782,8 @@ export class IssuanceService { fileUploadData.isError = false; fileUploadData.createDateTime = new Date(); fileUploadData.referenceId = jobDetails.data.email; + fileUploadData.jobId = jobDetails.id; + fileUploadData.clientId = jobDetails.clientId; try { const oobIssuancepayload: OutOfBandCredentialOfferPayload = { @@ -797,14 +818,24 @@ export class IssuanceService { fileUploadData.error = error.message; fileUploadData.detailError = `${JSON.stringify(error)}`; } - this.issuanceRepository.saveFileUploadData(fileUploadData); + await this.issuanceRepository.updateFileUploadData(fileUploadData); if (jobDetails.isLastData) { this.cacheManager.del(jobDetails.cacheId); - this.issuanceRepository.updateFileUploadDetails(jobDetails.fileUploadId, { + await this.issuanceRepository.updateFileUploadDetails(jobDetails.fileUploadId, { status: FileUploadStatus.completed, lastChangedDateTime: new Date() }); + + const socket = await io(`${process.env.SOCKET_HOST}`, { + reconnection: true, + reconnectionDelay: 5000, + reconnectionAttempts: Infinity, + autoConnect: true, + transports: ['websocket'] + }); + socket.emit('bulk-issuance-process-completed', { clientId: fileUploadData.clientId }); + } } From a7f41e840e70eac332be1b9be493b63577d91c7d Mon Sep 17 00:00:00 2001 From: tipusinghaw Date: Fri, 17 Nov 2023 17:05:31 +0530 Subject: [PATCH 31/62] fix: sonar cloud issue Signed-off-by: tipusinghaw --- apps/issuance/src/issuance.service.ts | 38 +++++++++++++++------------ 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/apps/issuance/src/issuance.service.ts b/apps/issuance/src/issuance.service.ts index 62fc6195d..cac90b0b7 100644 --- a/apps/issuance/src/issuance.service.ts +++ b/apps/issuance/src/issuance.service.ts @@ -729,23 +729,27 @@ export class IssuanceService { if (!respFile) { throw new BadRequestException('File data does not exist for the specific file'); } - await respFile.forEach(async (element, index) => { - this.logger.log(`element11----${JSON.stringify(element)}`); - const payload = - { - data: element.credential_data, - fileUploadId: element.fileUploadId, - clientId, - cacheId: requestId, - credentialDefinitionId: element.credDefId, - schemaLedgerId: element.schemaId, - orgId, - id: element.id, - isLastData: index === respFile.length - 1 - }; - this.processIssuanceData(payload); - - }); + for (const element of respFile) { + try { + this.logger.log(`element11----${JSON.stringify(element)}`); + const payload = { + data: element.credential_data, + fileUploadId: element.fileUploadId, + clientId, + cacheId: requestId, + credentialDefinitionId: element.credDefId, + schemaLedgerId: element.schemaId, + orgId, + id: element.id, + isLastData: respFile.indexOf(element) === respFile.length - 1 + }; + + this.processIssuanceData(payload); + } catch (error) { + // Handle errors if needed + this.logger.error(`Error processing issuance data: ${error}`); + } + } return 'Process initiated for bulk issuance'; } catch (error) { From 17b1fa445a0a5c8cb9dc00fa09174f224b415679 Mon Sep 17 00:00:00 2001 From: tipusinghaw Date: Mon, 20 Nov 2023 10:28:31 +0530 Subject: [PATCH 32/62] feat: implemented bulk-issuance retry Signed-off-by: tipusinghaw --- apps/api-gateway/src/enum.ts | 3 +- .../src/issuance/dtos/issuance.dto.ts | 2 +- .../src/issuance/issuance.controller.ts | 42 +++++- .../src/issuance/issuance.service.ts | 5 + apps/issuance/src/issuance.controller.ts | 5 + apps/issuance/src/issuance.repository.ts | 71 +++++++++- apps/issuance/src/issuance.service.ts | 128 +++++++++++++----- libs/common/src/response-messages/index.ts | 4 +- 8 files changed, 213 insertions(+), 47 deletions(-) diff --git a/apps/api-gateway/src/enum.ts b/apps/api-gateway/src/enum.ts index d3f8d2110..c6b6edca8 100644 --- a/apps/api-gateway/src/enum.ts +++ b/apps/api-gateway/src/enum.ts @@ -124,5 +124,6 @@ export enum FileUploadType { export enum FileUploadStatus { started = 'PROCESS_STARTED', completed = 'PROCESS_COMPLETED', - interrupted= 'PROCESS INTERRUPTED' + interrupted= 'PROCESS_INTERRUPTED', + retry= 'PROCESS_REINITIATED' } diff --git a/apps/api-gateway/src/issuance/dtos/issuance.dto.ts b/apps/api-gateway/src/issuance/dtos/issuance.dto.ts index 0785de5f6..6072d2964 100644 --- a/apps/api-gateway/src/issuance/dtos/issuance.dto.ts +++ b/apps/api-gateway/src/issuance/dtos/issuance.dto.ts @@ -208,7 +208,7 @@ export class FileParameter { } export class ClientDetails { - @ApiProperty({ required: false }) + @ApiProperty({ required: false, example: '68y647ayAv79879' }) @IsOptional() @Type(() => String) clientId = ''; diff --git a/apps/api-gateway/src/issuance/issuance.controller.ts b/apps/api-gateway/src/issuance/issuance.controller.ts index 70213e712..1103de121 100644 --- a/apps/api-gateway/src/issuance/issuance.controller.ts +++ b/apps/api-gateway/src/issuance/issuance.controller.ts @@ -328,7 +328,7 @@ export class IssuanceController { return res.status(HttpStatus.OK).json(finalResponse); } - @Get('/orgs/:orgId/bulk/files') + @Post('/orgs/:orgId/:requestId/bulk') @Roles(OrgRoles.OWNER, OrgRoles.ADMIN, OrgRoles.ISSUER, OrgRoles.VERIFIER) @UseGuards(AuthGuard('jwt'), OrgRolesGuard) @ApiBearerAuth() @@ -343,6 +343,35 @@ export class IssuanceController { description: 'Forbidden', type: ForbiddenErrorDto }) + @ApiOperation({ + summary: 'bulk issue credential', + description: 'bulk issue credential' + }) + async issueBulkCredentials(@Param('requestId') requestId: string, @Param('orgId') orgId: number, @Res() res: Response, @Body() clientDetails: ClientDetails): Promise { + const bulkIssunaceDetails = await this.issueCredentialService.issueBulkCredential(requestId, orgId, clientDetails.clientId); + const finalResponse: IResponseType = { + statusCode: HttpStatus.CREATED, + message: ResponseMessages.issuance.success.bulkIssuance, + data: bulkIssunaceDetails.response + }; + return res.status(HttpStatus.CREATED).json(finalResponse); + } + + @Get('/orgs/:orgId/bulk/files') + // @Roles(OrgRoles.OWNER, OrgRoles.ADMIN, OrgRoles.ISSUER, OrgRoles.VERIFIER) + // @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + // @ApiBearerAuth() + @ApiResponse({ status: 200, description: 'Success', type: ApiResponseDto }) + @ApiUnauthorizedResponse({ + status: 401, + description: 'Unauthorized', + type: UnauthorizedErrorDto + }) + @ApiForbiddenResponse({ + status: 403, + description: 'Forbidden', + type: ForbiddenErrorDto + }) @ApiOperation({ summary: 'Get the file list for bulk operation', description: 'Get all the file list for organization for bulk operation' @@ -454,7 +483,7 @@ export class IssuanceController { return res.status(HttpStatus.OK).json(finalResponse); } - @Post('/orgs/:orgId/:requestId/bulk') + @Post('/orgs/:orgId/:fileId/retry/bulk') @Roles(OrgRoles.OWNER, OrgRoles.ADMIN, OrgRoles.ISSUER, OrgRoles.VERIFIER) @UseGuards(AuthGuard('jwt'), OrgRolesGuard) @ApiBearerAuth() @@ -470,11 +499,11 @@ export class IssuanceController { type: ForbiddenErrorDto }) @ApiOperation({ - summary: 'bulk issue credential', - description: 'bulk issue credential' + summary: 'Retry bulk issue credential', + description: 'Retry bulk issue credential' }) - async issueBulkCredentials(@Param('requestId') requestId: string, @Param('orgId') orgId: number, @Res() res: Response, @Body() clientDetails: ClientDetails): Promise { - const bulkIssunaceDetails = await this.issueCredentialService.issueBulkCredential(requestId, orgId, clientDetails.clientId); + async retryBulkCredentials(@Param('fileId') fileId: string, @Param('orgId') orgId: number, @Res() res: Response, @Body() clientDetails: ClientDetails): Promise { + const bulkIssunaceDetails = await this.issueCredentialService.retryBulkCredential(fileId, orgId, clientDetails.clientId); const finalResponse: IResponseType = { statusCode: HttpStatus.CREATED, message: ResponseMessages.issuance.success.bulkIssuance, @@ -483,6 +512,7 @@ export class IssuanceController { return res.status(HttpStatus.CREATED).json(finalResponse); } + /** * Description: Issuer send credential to create offer * @param user diff --git a/apps/api-gateway/src/issuance/issuance.service.ts b/apps/api-gateway/src/issuance/issuance.service.ts index b79664a72..e74fc7ed8 100644 --- a/apps/api-gateway/src/issuance/issuance.service.ts +++ b/apps/api-gateway/src/issuance/issuance.service.ts @@ -111,4 +111,9 @@ export class IssuanceService extends BaseService { const payload = { requestId, orgId, clientId }; return this.sendNats(this.issuanceProxy, 'issue-bulk-credentials', payload); } + + async retryBulkCredential(fileId: string, orgId: number, clientId: string): Promise<{ response: object }> { + const payload = { fileId, orgId, clientId }; + return this.sendNats(this.issuanceProxy, 'retry-bulk-credentials', payload); + } } diff --git a/apps/issuance/src/issuance.controller.ts b/apps/issuance/src/issuance.controller.ts index fb7fb54ab..08115b2bb 100644 --- a/apps/issuance/src/issuance.controller.ts +++ b/apps/issuance/src/issuance.controller.ts @@ -87,4 +87,9 @@ export class IssuanceController { async issueBulkCredentials(payload: { requestId: string, orgId: number, clientId: string }): Promise { return this.issuanceService.issueBulkCredential(payload.requestId, payload.orgId, payload.clientId); } + + @MessagePattern({ cmd: 'retry-bulk-credentials' }) + async retryeBulkCredentials(payload: { fileId: string, orgId: number, clientId: string }): Promise { + return this.issuanceService.retryBulkCredential(payload.fileId, payload.orgId, payload.clientId); + } } diff --git a/apps/issuance/src/issuance.repository.ts b/apps/issuance/src/issuance.repository.ts index 394935941..ab13798c2 100644 --- a/apps/issuance/src/issuance.repository.ts +++ b/apps/issuance/src/issuance.repository.ts @@ -5,6 +5,7 @@ import { PrismaService } from '@credebl/prisma-service'; import { agent_invitations, credentials, file_data, file_upload, org_agents, organisation, platform_config, shortening_url } from '@prisma/client'; import { ResponseMessages } from '@credebl/common/response-messages'; import { FileUploadData, PreviewRequest, SchemaDetails } from '../interfaces/issuance.interfaces'; +import { FileUploadStatus } from 'apps/api-gateway/src/enum'; @Injectable() export class IssuanceRepository { @@ -234,8 +235,23 @@ export class IssuanceRepository { } } + async countErrorsForFile(fileUploadId: string): Promise { + try { + const errorCount = await this.prisma.file_data.count({ + where: { + fileUploadId, + isError: true + } + }); + + return errorCount; + } catch (error) { + this.logger.error(`[countErrorsForFile] - error: ${JSON.stringify(error)}`); + throw error; + } + } async getAllFileDetails(orgId: string, getAllfileDetails: PreviewRequest): Promise<{ - fileCount: number + fileCount: number; fileList: { id: string; name: string; @@ -247,7 +263,9 @@ export class IssuanceRepository { lastChangedDateTime: Date; lastChangedBy: string; deletedAt: Date; - }[] + failedRecords: number; + totalRecords: number; + }[]; }> { try { const fileList = await this.prisma.file_upload.findMany({ @@ -262,12 +280,27 @@ export class IssuanceRepository { take: Number(getAllfileDetails?.pageSize), skip: (getAllfileDetails?.pageNumber - 1) * getAllfileDetails?.pageSize }); + + const fileListWithDetails = await Promise.all( + fileList.map(async (file) => { + const failedRecords = await this.countErrorsForFile(file.id); + const totalRecords = await this.prisma.file_data.count({ + where: { + fileUploadId: file.id + } + }); + const successfulRecords = totalRecords - failedRecords; + return { ...file, failedRecords, totalRecords, successfulRecords }; + }) + ); + const fileCount = await this.prisma.file_upload.count({ where: { orgId: String(orgId) } }); - return { fileCount, fileList }; + + return { fileCount, fileList: fileListWithDetails }; } catch (error) { this.logger.error(`[getFileUploadDetails] - error: ${JSON.stringify(error)}`); throw error; @@ -319,7 +352,6 @@ export class IssuanceRepository { async updateFileUploadData(fileUploadData: FileUploadData): Promise { try { const { jobId, fileUpload, isError, referenceId, error, detailError } = fileUploadData; - if (jobId) { return this.prisma.file_data.update({ where: { id: jobId }, @@ -373,4 +405,35 @@ export class IssuanceRepository { throw error; } } + + async getFailedCredentials(fileId: string): Promise { + try { + return this.prisma.file_data.findMany({ + where: { + fileUploadId: fileId, + isError: true + } + }); + } catch (error) { + this.logger.error(`[getFileDetails] - error: ${JSON.stringify(error)}`); + throw error; + } + } + + async updateFileUploadStatus(fileId: string): Promise { + try { + return this.prisma.file_upload.update({ + where: { + id: fileId + }, + data: { + status: FileUploadStatus.retry + } + }); + + } catch (error) { + this.logger.error(`[updateFileUploadStatus] - error: ${JSON.stringify(error)}`); + throw error; + } + } } \ No newline at end of file diff --git a/apps/issuance/src/issuance.service.ts b/apps/issuance/src/issuance.service.ts index cac90b0b7..62e359c53 100644 --- a/apps/issuance/src/issuance.service.ts +++ b/apps/issuance/src/issuance.service.ts @@ -665,7 +665,7 @@ export class IssuanceService { } } - async issueBulkCredential(requestId: string, orgId: number, clientId:string): Promise { + async issueBulkCredential(requestId: string, orgId: number, clientId: string): Promise { const fileUpload: { lastChangedDateTime: Date; name?: string; @@ -711,27 +711,27 @@ export class IssuanceService { const saveFileDetailsPromises = parsedData.map(async (element) => { const credentialPayload = { - credential_data: element, - schemaId: parsedPrimeDetails.schemaLedgerId, - credDefId: parsedPrimeDetails.credentialDefinitionId, - state: false, - isError: false, - fileUploadId: respFileUpload.id + credential_data: element, + schemaId: parsedPrimeDetails.schemaLedgerId, + credDefId: parsedPrimeDetails.credentialDefinitionId, + state: false, + isError: false, + fileUploadId: respFileUpload.id }; return this.issuanceRepository.saveFileDetails(credentialPayload); - }); - - // Wait for all saveFileDetails operations to complete - await Promise.all(saveFileDetailsPromises); - - // Now fetch the file details - const respFile = await this.issuanceRepository.getFileDetails(respFileUpload.id); + }); + + // Wait for all saveFileDetails operations to complete + await Promise.all(saveFileDetailsPromises); + + // Now fetch the file details + const respFile = await this.issuanceRepository.getFileDetails(respFileUpload.id); if (!respFile) { - throw new BadRequestException('File data does not exist for the specific file'); + throw new BadRequestException(ResponseMessages.issuance.error.fileData); } for (const element of respFile) { try { - this.logger.log(`element11----${JSON.stringify(element)}`); + this.logger.log(`element----${JSON.stringify(element)}`); const payload = { data: element.credential_data, fileUploadId: element.fileUploadId, @@ -739,14 +739,14 @@ export class IssuanceService { cacheId: requestId, credentialDefinitionId: element.credDefId, schemaLedgerId: element.schemaId, + isRetry: false, orgId, id: element.id, isLastData: respFile.indexOf(element) === respFile.length - 1 }; - - this.processIssuanceData(payload); + + this.processIssuanceData(payload); } catch (error) { - // Handle errors if needed this.logger.error(`Error processing issuance data: ${error}`); } } @@ -764,6 +764,55 @@ export class IssuanceService { } } + async retryBulkCredential(fileId: string, orgId: number, clientId: string): Promise { + let respFile; + let respFileUpload; + + try { + respFileUpload = await this.issuanceRepository.updateFileUploadStatus(fileId); + respFile = await this.issuanceRepository.getFailedCredentials(fileId); + + if (!respFile || 0 === respFile.length) { + throw new BadRequestException(ResponseMessages.issuance.error.retry); + } + + for (const element of respFile) { + try { + this.logger.log(`element----${JSON.stringify(element)}`); + const payload = { + data: element.credential_data, + fileUploadId: element.fileUploadId, + clientId, + credentialDefinitionId: element.credDefId, + schemaLedgerId: element.schemaId, + orgId, + id: element.id, + isRetry: true, + isLastData: respFile.indexOf(element) === respFile.length - 1 + }; + + await this.processIssuanceData(payload); + } catch (error) { + // Handle errors if needed + this.logger.error(`Error processing issuance data: ${error}`); + } + } + + return 'Process reinitiated for bulk issuance'; + } catch (error) { + throw new error; + } finally { + // Update file upload details in the database + if (respFileUpload && respFileUpload.id) { + const fileUpload = { + status: FileUploadStatus.interrupted, + lastChangedDateTime: new Date() + }; + + await this.issuanceRepository.updateFileUploadDetails(respFileUpload.id, fileUpload); + } + } + } // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types async processIssuanceData(jobDetails): Promise { @@ -824,23 +873,34 @@ export class IssuanceService { } await this.issuanceRepository.updateFileUploadData(fileUploadData); - if (jobDetails.isLastData) { - this.cacheManager.del(jobDetails.cacheId); - await this.issuanceRepository.updateFileUploadDetails(jobDetails.fileUploadId, { - status: FileUploadStatus.completed, - lastChangedDateTime: new Date() - }); - - const socket = await io(`${process.env.SOCKET_HOST}`, { - reconnection: true, - reconnectionDelay: 5000, - reconnectionAttempts: Infinity, - autoConnect: true, - transports: ['websocket'] - }); - socket.emit('bulk-issuance-process-completed', { clientId: fileUploadData.clientId }); + try { + if (jobDetails.isLastData) { + if (!jobDetails.isRetry) { + this.cacheManager.del(jobDetails.cacheId); + await this.issuanceRepository.updateFileUploadDetails(jobDetails.fileUploadId, { + status: FileUploadStatus.completed, + lastChangedDateTime: new Date() + }); + } else { + await this.issuanceRepository.updateFileUploadDetails(jobDetails.fileUploadId, { + status: FileUploadStatus.completed, + lastChangedDateTime: new Date() + }); + } + const socket = await io(`${process.env.SOCKET_HOST}`, { + reconnection: true, + reconnectionDelay: 5000, + reconnectionAttempts: Infinity, + autoConnect: true, + transports: ['websocket'] + }); + socket.emit('bulk-issuance-process-completed', { clientId: fileUploadData.clientId }); + } + } catch (error) { + this.logger.error(`Error completing bulk issuance process: ${error}`); } + } async validateFileHeaders( diff --git a/libs/common/src/response-messages/index.ts b/libs/common/src/response-messages/index.ts index 9c3c29bd5..a4f18d252 100644 --- a/libs/common/src/response-messages/index.ts +++ b/libs/common/src/response-messages/index.ts @@ -182,7 +182,9 @@ export const ResponseMessages = { previewCachedData: 'Error while fetching cached data', emptyFileData: 'File details does not exit or removed', cacheTimeOut: 'Timeout for reviewing data, re-upload your file and generate new request', - fileNotFound: 'File details not found' + fileNotFound: 'File details not found', + fileData: 'File data does not exist for the specific file', + retry: 'Credentials do not exist for retry' } }, verification: { From 5fd9a9800aafb2b261487fc68f8de208f22d3c97 Mon Sep 17 00:00:00 2001 From: tipusinghaw Date: Mon, 20 Nov 2023 10:32:21 +0530 Subject: [PATCH 33/62] fix: removed commented code Signed-off-by: tipusinghaw --- apps/api-gateway/src/issuance/issuance.controller.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/api-gateway/src/issuance/issuance.controller.ts b/apps/api-gateway/src/issuance/issuance.controller.ts index 1103de121..b701d88c7 100644 --- a/apps/api-gateway/src/issuance/issuance.controller.ts +++ b/apps/api-gateway/src/issuance/issuance.controller.ts @@ -358,9 +358,9 @@ export class IssuanceController { } @Get('/orgs/:orgId/bulk/files') - // @Roles(OrgRoles.OWNER, OrgRoles.ADMIN, OrgRoles.ISSUER, OrgRoles.VERIFIER) - // @UseGuards(AuthGuard('jwt'), OrgRolesGuard) - // @ApiBearerAuth() + @Roles(OrgRoles.OWNER, OrgRoles.ADMIN, OrgRoles.ISSUER, OrgRoles.VERIFIER) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + @ApiBearerAuth() @ApiResponse({ status: 200, description: 'Success', type: ApiResponseDto }) @ApiUnauthorizedResponse({ status: 401, From 44c6cae94ad20caa4c47abfb77e95405129720c5 Mon Sep 17 00:00:00 2001 From: tipusinghaw Date: Mon, 20 Nov 2023 14:20:24 +0530 Subject: [PATCH 34/62] fix: multiple aattribute issue Signed-off-by: tipusinghaw --- apps/issuance/src/issuance.service.ts | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/apps/issuance/src/issuance.service.ts b/apps/issuance/src/issuance.service.ts index 62e359c53..3b16c7223 100644 --- a/apps/issuance/src/issuance.service.ts +++ b/apps/issuance/src/issuance.service.ts @@ -839,25 +839,19 @@ export class IssuanceService { fileUploadData.clientId = jobDetails.clientId; try { - const oobIssuancepayload: OutOfBandCredentialOfferPayload = { - credentialDefinitionId: '', - orgId: 0 + const oobIssuancepayload = { + credentialDefinitionId: jobDetails.credentialDefinitionId, + orgId: jobDetails.orgId, + attributes: [], + emailId: jobDetails.data.email }; - + for (const key in jobDetails.data) { - if (jobDetails.data.hasOwnProperty(key)) { + if (jobDetails.data.hasOwnProperty(key) && 'email' !== key) { const value = jobDetails.data[key]; - // eslint-disable-next-line no-unused-expressions - if ('email' !== key) { - oobIssuancepayload['attributes'] = [{ name: key, value }]; - } else { - oobIssuancepayload['emailId'] = value; - } - + oobIssuancepayload.attributes.push({ name: key, value }); } } - oobIssuancepayload['credentialDefinitionId'] = jobDetails.credentialDefinitionId; - oobIssuancepayload['orgId'] = jobDetails.orgId; const oobCredentials = await this.outOfBandCredentialOffer( oobIssuancepayload From 9946fb6c696524d192577055d464049a953e3f0a Mon Sep 17 00:00:00 2001 From: bhavanakarwade Date: Mon, 20 Nov 2023 19:21:20 +0530 Subject: [PATCH 35/62] feat: developed post and get api for share user certificate Signed-off-by: bhavanakarwade --- apps/api-gateway/src/user/user.controller.ts | 7 +- apps/api-gateway/src/user/user.service.ts | 4 +- apps/user/repositories/user.repository.ts | 17 ++- apps/user/src/user.controller.ts | 2 +- apps/user/src/user.service.ts | 90 ++++++++------ apps/user/templates/arbiter-template.ts | 8 +- apps/user/templates/participant-template.ts | 56 +++++++-- apps/user/templates/winner-template.ts | 112 +++++++++++++++--- libs/aws/src/aws.service.ts | 7 +- libs/common/src/response-messages/index.ts | 6 +- .../migration.sql | 12 ++ libs/prisma-service/prisma/schema.prisma | 2 +- 12 files changed, 231 insertions(+), 92 deletions(-) create mode 100644 libs/prisma-service/prisma/migrations/20231120132943_credential_id_unique/migration.sql diff --git a/apps/api-gateway/src/user/user.controller.ts b/apps/api-gateway/src/user/user.controller.ts index 16941a59b..a4ff3a898 100644 --- a/apps/api-gateway/src/user/user.controller.ts +++ b/apps/api-gateway/src/user/user.controller.ts @@ -257,11 +257,11 @@ export class UserController { return res.status(HttpStatus.OK).json(finalResponse); } - @Get('/user-credentials/:id') + @Get('/user-credentials/:credentialId') @ApiOperation({ summary: 'Get user credentials by Id', description: 'Get user credentials by Id' }) @ApiResponse({ status: 200, description: 'Success', type: ApiResponseDto }) - async getUserCredentialsById (@Param('id') id: string, @Res() res: Response): Promise { - const getUserCrdentialsById = await this.userService.getUserCredentialsById(id); + async getUserCredentialsById (@Param('credentialId') credentialId: string, @Res() res: Response): Promise { + const getUserCrdentialsById = await this.userService.getUserCredentialsById(credentialId); const finalResponse: IResponseType = { statusCode: HttpStatus.OK, @@ -271,7 +271,6 @@ export class UserController { return res.status(HttpStatus.OK).json(finalResponse); } - /** * * @param acceptRejectInvitation diff --git a/apps/api-gateway/src/user/user.service.ts b/apps/api-gateway/src/user/user.service.ts index 9b682a3e8..4d45bd159 100644 --- a/apps/api-gateway/src/user/user.service.ts +++ b/apps/api-gateway/src/user/user.service.ts @@ -27,8 +27,8 @@ export class UserService extends BaseService { } - async getUserCredentialsById(id: string): Promise<{ response: object }> { - const payload = { id }; + async getUserCredentialsById(credentialId: string): Promise<{ response: object }> { + const payload = { credentialId }; return this.sendNats(this.serviceProxy, 'get-user-credentials-by-id', payload); } diff --git a/apps/user/repositories/user.repository.ts b/apps/user/repositories/user.repository.ts index c407062c2..feefc6c61 100644 --- a/apps/user/repositories/user.repository.ts +++ b/apps/user/repositories/user.repository.ts @@ -108,14 +108,14 @@ export class UserRepository { * @param id * @returns User profile data */ - async getUserCredentialsById(id: string): Promise { - return this.prisma.user_credentials.findUnique({ - where: { - id - } - }); - } - + async getUserCredentialsById(credentialId: string): Promise { + return this.prisma.user_credentials.findUnique({ + where: { + credentialId + } + }); + } + /** * * @param id @@ -128,7 +128,6 @@ export class UserRepository { return this.findUserForPublicProfile(queryOptions); } - /** * diff --git a/apps/user/src/user.controller.ts b/apps/user/src/user.controller.ts index e21c85a73..f4087c1bd 100644 --- a/apps/user/src/user.controller.ts +++ b/apps/user/src/user.controller.ts @@ -63,7 +63,7 @@ export class UserController { @MessagePattern({ cmd: 'get-user-credentials-by-id' }) - async getUserCredentialsById(payload: { id }): Promise { + async getUserCredentialsById(payload: { credentialId }): Promise { return this.userService.getUserCredentialsById(payload); } diff --git a/apps/user/src/user.service.ts b/apps/user/src/user.service.ts index 83ff2cee8..e6e0b7006 100644 --- a/apps/user/src/user.service.ts +++ b/apps/user/src/user.service.ts @@ -296,7 +296,6 @@ export class UserService { } } - private validateEmail(email: string): void { if (!validator.isEmail(email)) { throw new UnauthorizedException(ResponseMessages.user.error.invalidEmail); @@ -311,35 +310,33 @@ export class UserService { async login(loginUserDto: LoginUserDto): Promise { const { email, password, isPasskey } = loginUserDto; - try { - this.validateEmail(email); - const userData = await this.userRepository.checkUserExist(email); - if (!userData) { - throw new NotFoundException(ResponseMessages.user.error.notFound); - } - - if (userData && !userData.isEmailVerified) { - throw new BadRequestException(ResponseMessages.user.error.verifyMail); - } - - if (true === isPasskey && false === userData?.isFidoVerified) { - throw new UnauthorizedException(ResponseMessages.user.error.registerFido); - } - - if (true === isPasskey && userData?.username && true === userData?.isFidoVerified) { - const getUserDetails = await this.userRepository.getUserDetails(userData.email); - const decryptedPassword = await this.commonService.decryptPassword(getUserDetails.password); - return this.generateToken(email, decryptedPassword); - } else { - const decryptedPassword = await this.commonService.decryptPassword(password); - return this.generateToken(email, decryptedPassword); - } - } catch (error) { - this.logger.error(`In Login User : ${JSON.stringify(error)}`); - throw new RpcException(error.response ? error.response : error); + try { + this.validateEmail(email); + const userData = await this.userRepository.checkUserExist(email); + if (!userData) { + throw new NotFoundException(ResponseMessages.user.error.notFound); } - - + + if (userData && !userData.isEmailVerified) { + throw new BadRequestException(ResponseMessages.user.error.verifyMail); + } + + if (true === isPasskey && false === userData?.isFidoVerified) { + throw new UnauthorizedException(ResponseMessages.user.error.registerFido); + } + + if (true === isPasskey && userData?.username && true === userData?.isFidoVerified) { + const getUserDetails = await this.userRepository.getUserDetails(userData.email); + const decryptedPassword = await this.commonService.decryptPassword(getUserDetails.password); + return this.generateToken(email, decryptedPassword); + } else { + const decryptedPassword = await this.commonService.decryptPassword(password); + return this.generateToken(email, decryptedPassword); + } + } catch (error) { + this.logger.error(`In Login User : ${JSON.stringify(error)}`); + throw new RpcException(error.response ? error.response : error); + } } async generateToken(email: string, password: string): Promise { @@ -400,9 +397,12 @@ export class UserService { } } - async getUserCredentialsById(payload: { id }): Promise { + async getUserCredentialsById(payload: { credentialId }): Promise { try { - const userCredentials = await this.userRepository.getUserCredentialsById(payload.id); + const userCredentials = await this.userRepository.getUserCredentialsById(payload.credentialId); + if (!userCredentials) { + throw new NotFoundException(ResponseMessages.user.error.credentialNotFound); + } return userCredentials; } catch (error) { this.logger.error(`get user: ${JSON.stringify(error)}`); @@ -584,11 +584,28 @@ export class UserService { const imageBuffer = await this.convertHtmlToImage(template, shareUserCertificate.credentialId); const verifyCode = uuidv4(); - // const myFile = new File([readFileSync(imageBuffer)], `cert_${shareUserCertificate.credentialId}.jpeg`); - const imageUrl = await this.awsService.uploads3(imageBuffer, 'jpeg', verifyCode, 'certificates', 'base64'); + const imageUrl = await this.awsService.uploadUserCertificate( + imageBuffer, + 'jpeg', + verifyCode, + 'certificates', + 'base64' + ); - return this.saveCertificateUrl(imageUrl, shareUserCertificate.credentialId); + const existCredentialId = await this.userRepository.getUserCredentialsById(shareUserCertificate.credentialId); + + if (existCredentialId) { + return `${process.env.FRONT_END_URL}/certificates/${shareUserCertificate.credentialId}`; + } + + const saveCredentialData = await this.saveCertificateUrl(imageUrl, shareUserCertificate.credentialId); + + if (!saveCredentialData) { + throw new BadRequestException(ResponseMessages.schema.error.notStoredCredential); + } + + return `${process.env.FRONT_END_URL}/certificates/${shareUserCertificate.credentialId}`; } async saveCertificateUrl(imageUrl: string, credentialId: string): Promise { @@ -600,12 +617,11 @@ export class UserService { const page = await browser.newPage(); await page.setContent(template); - const filename = `cert_${credentialId}.jpeg`; - const screenshot = await page.screenshot({ path: filename }); + const screenshot = await page.screenshot(); await browser.close(); return screenshot; } - + /** * * @param acceptRejectInvitation diff --git a/apps/user/templates/arbiter-template.ts b/apps/user/templates/arbiter-template.ts index 29eac7a2e..b2be9e502 100644 --- a/apps/user/templates/arbiter-template.ts +++ b/apps/user/templates/arbiter-template.ts @@ -1,5 +1,9 @@ +import { Attribute } from "../interfaces/user.interface"; + export class ArbiterTemplate { - public getArbiterTemplate(attributes: object): string { + public getArbiterTemplate(attributes: Attribute[]): string { + const name = 0 < attributes.length ? attributes[0].name : ''; + try { return ` @@ -11,7 +15,7 @@ export class ArbiterTemplate {
👩‍⚖️
-

Thank You, ${attributes}!

+

Thank You, ${name}!

Your role as ${attributes} is essential in our contest.

diff --git a/apps/user/templates/participant-template.ts b/apps/user/templates/participant-template.ts index 84b85eff9..0b372658c 100644 --- a/apps/user/templates/participant-template.ts +++ b/apps/user/templates/participant-template.ts @@ -1,22 +1,54 @@ +import { Attribute } from "../interfaces/user.interface"; + export class ParticipantTemplate { - public getParticipantTemplate(attributes: object): string { - try { - return ` + public getParticipantTemplate(attributes: Attribute[]): string { + try { + const nameAttribute = attributes.find(attr => 'full_name' in attr); + const countryAttribute = attributes.find(attr => 'country' in attr); + const positionAttribute = attributes.find(attr => 'position' in attr); + const issuedByAttribute = attributes.find(attr => 'issued_by' in attr); + const categoryAttribute = attributes.find(attr => 'category' in attr); + const dateAttribute = attributes.find(attr => 'issued_date' in attr); + + const name = nameAttribute ? nameAttribute['full_name'] : ''; + const country = countryAttribute ? countryAttribute['country'] : ''; + const position = positionAttribute ? positionAttribute['position'] : ''; + const issuedBy = issuedByAttribute ? issuedByAttribute['issued_by'] : ''; + const category = categoryAttribute ? categoryAttribute['category'] : ''; + const date = dateAttribute ? dateAttribute['issued_date'] : ''; + + return ` - Participant Template + Certificate of Achievement - -
-
🎉
-

Thank You, ${attributes}!

-

You're a valued ${attributes} in our contest.

+ + +
+
🏆
+

Certificate of Achievement

+ +

${name}

+

has demonstrated outstanding performance and successfully completed the requirements for

+ +

Participant

+

Position: ${position}

+

Issued by: ${issuedBy}

+ +
+ +

Country: ${country}

+

Category: ${category}

+ +

Issued Date: ${date}

+ +

Congratulations!

+ `; - } catch (error) { - } - } + } catch (error) {} + } } diff --git a/apps/user/templates/winner-template.ts b/apps/user/templates/winner-template.ts index 8092c1f62..8266ad6d4 100644 --- a/apps/user/templates/winner-template.ts +++ b/apps/user/templates/winner-template.ts @@ -1,24 +1,102 @@ import { Attribute } from '../interfaces/user.interface'; export class WinnerTemplate { - public getWinnerTemplate(attributes: Attribute[]): string { + findAttributeByName(attributes: Attribute[], name: string): Attribute { + return attributes.find((attr) => name in attr); + } + + async getWinnerTemplate(attributes: Attribute[]): Promise { try { - const name = 0 < attributes.length ? attributes[0].name : ''; + const [name, country, position, issuedBy, category, date] = await Promise.all(attributes).then((attributes) => { + const name = this.findAttributeByName(attributes, 'full_name')?.full_name ?? ''; + const country = this.findAttributeByName(attributes, 'country')?.country ?? ''; + const position = this.findAttributeByName(attributes, 'position')?.position ?? ''; + const issuedBy = this.findAttributeByName(attributes, 'issued_by')?.issued_by ?? ''; + const category = this.findAttributeByName(attributes, 'category')?.category ?? ''; + const date = this.findAttributeByName(attributes, 'issued_date')?.issued_date ?? ''; + return [name, country, position, issuedBy, category, date]; + }); return ` - - - - - Winner Template - - -
-
🏆
-

Congratulations, ${name}!

-

You're the Winner of our contest.

-
- - `; - } catch (error) {} + + + + + Certificate of Achievement + + + +
+
🏆
+

Certificate of Achievement

+ +

${name}

+

has demonstrated outstanding performance and successfully completed the requirements for

+ +

Winner

+

Position: ${position}

+

Issued by: ${issuedBy}

+ +
+ +

Country: ${country}

+

Category: ${category}

+ +

Issued Date: ${date}

+ +

Congratulations!

+
+ + `; + } catch {} } } diff --git a/libs/aws/src/aws.service.ts b/libs/aws/src/aws.service.ts index 45089b636..443ab7328 100644 --- a/libs/aws/src/aws.service.ts +++ b/libs/aws/src/aws.service.ts @@ -15,7 +15,7 @@ export class AwsService { }); } - async uploads3( + async uploadUserCertificate( fileBuffer: Buffer, ext: string, verifyCode: string, @@ -34,10 +34,7 @@ export class AwsService { ContentEncoding: encoding, ContentType: `image/jpeg` }); - - // return `https://${process.env.AWS_PUBLIC_BUCKET_NAME}.s3.${process.env.AWS_PUBLIC_REGION}.amazonaws.com/${pathAWS}/${encodeURIComponent(filename)}.${timestamp}.${ext}`; - return `${process.env.FRONT_END_URL}/certificates/${verifyCode}/${encodeURIComponent(filename)}.${timestamp}.${ext}`; - + return `https://${process.env.AWS_PUBLIC_BUCKET_NAME}.s3.${process.env.AWS_PUBLIC_REGION}.amazonaws.com/${pathAWS}/${encodeURIComponent(filename)}.${timestamp}.${ext}`; } catch (err) { throw new HttpException('An error occurred while uploading the image', HttpStatus.SERVICE_UNAVAILABLE); } diff --git a/libs/common/src/response-messages/index.ts b/libs/common/src/response-messages/index.ts index 501cd9aba..661865592 100644 --- a/libs/common/src/response-messages/index.ts +++ b/libs/common/src/response-messages/index.ts @@ -42,7 +42,8 @@ export const ResponseMessages = { verifyEmail: 'The verification link has already been sent to your email address. please verify', emailNotVerified: 'The verification link has already been sent to your email address. please verify', userNotRegisterd: 'The user has not yet completed the registration process', - InvalidEmailDomain :'Email from this domain is not allowed' + InvalidEmailDomain :'Email from this domain is not allowed', + credentialNotFound: 'User credentials not found' } }, organisation: { @@ -107,7 +108,8 @@ export const ResponseMessages = { notCreated: 'Schema not created', notFound: 'Schema records not found', schemaIdNotFound: 'SchemaLedgerId not found', - credentialDefinitionNotFound: 'No credential definition exist' + credentialDefinitionNotFound: 'No credential definition exist', + notStoredCredential: 'User credential not stored' } }, credentialDefinition: { diff --git a/libs/prisma-service/prisma/migrations/20231120132943_credential_id_unique/migration.sql b/libs/prisma-service/prisma/migrations/20231120132943_credential_id_unique/migration.sql new file mode 100644 index 000000000..535e03894 --- /dev/null +++ b/libs/prisma-service/prisma/migrations/20231120132943_credential_id_unique/migration.sql @@ -0,0 +1,12 @@ +/* + Warnings: + + - A unique constraint covering the columns `[credentialId]` on the table `user_credentials` will be added. If there are existing duplicate values, this will fail. + - Made the column `credentialId` on table `user_credentials` required. This step will fail if there are existing NULL values in that column. + +*/ +-- AlterTable +ALTER TABLE "user_credentials" ALTER COLUMN "credentialId" SET NOT NULL; + +-- CreateIndex +CREATE UNIQUE INDEX "user_credentials_credentialId_key" ON "user_credentials"("credentialId"); diff --git a/libs/prisma-service/prisma/schema.prisma b/libs/prisma-service/prisma/schema.prisma index 936defbae..90daa09f3 100644 --- a/libs/prisma-service/prisma/schema.prisma +++ b/libs/prisma-service/prisma/schema.prisma @@ -52,7 +52,7 @@ model user_activity { model user_credentials { id String @id @default(uuid()) imageUrl String? - credentialId String? + credentialId String @unique createDateTime DateTime @default(now()) @db.Timestamptz(6) createdBy String @default("1") lastChangedDateTime DateTime @default(now()) @db.Timestamptz(6) From ccc677d048f7a7fe6c7bfad53f47916870318028 Mon Sep 17 00:00:00 2001 From: tipusinghaw Date: Mon, 20 Nov 2023 19:33:33 +0530 Subject: [PATCH 36/62] fix: socket issue and error handling Signed-off-by: tipusinghaw --- apps/api-gateway/src/authz/socket.gateway.ts | 4 +- .../interfaces/issuance.interfaces.ts | 1 - apps/issuance/src/issuance.repository.ts | 29 ++++++++ apps/issuance/src/issuance.service.ts | 69 +++++++++---------- libs/common/src/response-messages/index.ts | 6 +- 5 files changed, 68 insertions(+), 41 deletions(-) diff --git a/apps/api-gateway/src/authz/socket.gateway.ts b/apps/api-gateway/src/authz/socket.gateway.ts index 7e46ffcea..bfb623de8 100644 --- a/apps/api-gateway/src/authz/socket.gateway.ts +++ b/apps/api-gateway/src/authz/socket.gateway.ts @@ -96,7 +96,7 @@ export class SocketGateway implements OnGatewayConnection { } @SubscribeMessage('error-in-wallet-creation-process') - async handleErrorResponse(payload: ISocketInterface): Promise { + async handleErrorResponse(client:string, payload: ISocketInterface): Promise { this.logger.log(`error-in-wallet-creation-process ${payload.clientId}`); this.server .to(payload.clientId) @@ -104,7 +104,7 @@ export class SocketGateway implements OnGatewayConnection { } @SubscribeMessage('bulk-issuance-process-completed') - async handleBulkIssuance(payload: ISocketInterface): Promise { + async handleBulkIssuance(client:string, payload: ISocketInterface): Promise { this.logger.log(`bulk-issuance-process-completed ${payload.clientId}`); this.server .to(payload.clientId) diff --git a/apps/issuance/interfaces/issuance.interfaces.ts b/apps/issuance/interfaces/issuance.interfaces.ts index a5f02ba08..d1f3a04d1 100644 --- a/apps/issuance/interfaces/issuance.interfaces.ts +++ b/apps/issuance/interfaces/issuance.interfaces.ts @@ -101,5 +101,4 @@ export interface FileUploadData { error?: string; detailError?: string; jobId: string; - clientId: string; } \ No newline at end of file diff --git a/apps/issuance/src/issuance.repository.ts b/apps/issuance/src/issuance.repository.ts index ab13798c2..fa2ee19c4 100644 --- a/apps/issuance/src/issuance.repository.ts +++ b/apps/issuance/src/issuance.repository.ts @@ -371,6 +371,21 @@ export class IssuanceRepository { throw error; } } + async deleteFileDataByJobId(jobId: string): Promise { + try { + if (jobId) { + return this.prisma.file_data.update({ + where: { id: jobId }, + data: { + credential_data: null + } + }); + } + } catch (error) { + this.logger.error(`[saveFileUploadData] - error: ${JSON.stringify(error)}`); + throw error; + } + } // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/explicit-function-return-type, @typescript-eslint/no-unused-vars async saveFileDetails(fileData) { @@ -436,4 +451,18 @@ export class IssuanceRepository { throw error; } } + + async getFileDetailsById(fileId: string): Promise { + try { + return this.prisma.file_upload.findUnique({ + where: { + id: fileId + } + }); + + } catch (error) { + this.logger.error(`[updateFileUploadStatus] - error: ${JSON.stringify(error)}`); + throw error; + } + } } \ No newline at end of file diff --git a/apps/issuance/src/issuance.service.ts b/apps/issuance/src/issuance.service.ts index 3b16c7223..b6897810c 100644 --- a/apps/issuance/src/issuance.service.ts +++ b/apps/issuance/src/issuance.service.ts @@ -1,3 +1,4 @@ +/* eslint-disable no-useless-catch */ /* eslint-disable camelcase */ import { CommonService } from '@credebl/common'; import { BadRequestException, HttpException, Inject, Injectable, Logger, NotFoundException } from '@nestjs/common'; @@ -555,8 +556,8 @@ export class IssuanceService { ); } - this.validateFileHeaders(fileHeader, attributeNameArray, importFileDetails); - this.validateFileData(fileData); + await this.validateFileHeaders(fileHeader, attributeNameArray); + await this.validateFileData(fileData); const resData = { schemaLedgerId: credDefResponse.schemaLedgerId, @@ -572,7 +573,7 @@ export class IssuanceService { } catch (error) { this.logger.error(`error in validating credentials : ${error}`); - throw new RpcException(error.response); + throw new RpcException(error.response ? error.response : error); } finally { // await this.awsService.deleteFile(importFileDetails.fileKey); // this.logger.error(`Deleted uploaded file after processing.`); @@ -688,7 +689,6 @@ export class IssuanceService { } this.logger.log(`requestId----${JSON.stringify(requestId)}`); - try { const cachedData = await this.cacheManager.get(requestId); this.logger.log(`cachedData----${JSON.stringify(cachedData)}`); @@ -769,11 +769,18 @@ export class IssuanceService { let respFileUpload; try { + + const fileDetails = await this.issuanceRepository.getFileDetailsById(fileId); + if (!fileDetails) { + throw new BadRequestException(ResponseMessages.issuance.error.retry); + } + respFileUpload = await this.issuanceRepository.updateFileUploadStatus(fileId); respFile = await this.issuanceRepository.getFailedCredentials(fileId); - if (!respFile || 0 === respFile.length) { - throw new BadRequestException(ResponseMessages.issuance.error.retry); + if (0 === respFile.length) { + const errorMessage = ResponseMessages.bulkIssuance.error.fileDetailsNotFound; + throw new BadRequestException(`${errorMessage}`); } for (const element of respFile) { @@ -800,7 +807,7 @@ export class IssuanceService { return 'Process reinitiated for bulk issuance'; } catch (error) { - throw new error; + throw new RpcException(error.response ? error.response : error); } finally { // Update file upload details in the database if (respFileUpload && respFileUpload.id) { @@ -825,8 +832,7 @@ export class IssuanceService { createDateTime: undefined, error: '', detailError: '', - jobId: '', - clientId: '' + jobId: '' }; this.logger.log(`jobDetails----${JSON.stringify(jobDetails)}`); @@ -836,7 +842,6 @@ export class IssuanceService { fileUploadData.createDateTime = new Date(); fileUploadData.referenceId = jobDetails.data.email; fileUploadData.jobId = jobDetails.id; - fileUploadData.clientId = jobDetails.clientId; try { const oobIssuancepayload = { @@ -845,7 +850,7 @@ export class IssuanceService { attributes: [], emailId: jobDetails.data.email }; - + for (const key in jobDetails.data) { if (jobDetails.data.hasOwnProperty(key) && 'email' !== key) { const value = jobDetails.data[key]; @@ -856,6 +861,9 @@ export class IssuanceService { const oobCredentials = await this.outOfBandCredentialOffer( oobIssuancepayload ); + if (oobCredentials) { + await this.issuanceRepository.deleteFileDataByJobId(jobDetails.id); + } return oobCredentials; } catch (error) { this.logger.error( @@ -889,7 +897,7 @@ export class IssuanceService { autoConnect: true, transports: ['websocket'] }); - socket.emit('bulk-issuance-process-completed', { clientId: fileUploadData.clientId }); + socket.emit('bulk-issuance-process-completed', { clientId: jobDetails.clientId }); } } catch (error) { this.logger.error(`Error completing bulk issuance process: ${error}`); @@ -899,47 +907,34 @@ export class IssuanceService { async validateFileHeaders( fileHeader: string[], - schemaAttributes, - payload + schemaAttributes: string[] ): Promise { try { - const fileSchemaHeader: string[] = fileHeader.slice(); + if ('email' === fileHeader[0]) { fileSchemaHeader.splice(0, 1); } else { - throw new BadRequestException( - `1st column of the file should always be 'reference_id'` + throw new BadRequestException(ResponseMessages.bulkIssuance.error.emailColumn ); } + if (schemaAttributes.length !== fileSchemaHeader.length) { - throw new BadRequestException( - `Number of supplied values is different from the number of schema attributes defined for '${payload.credDefId}'` + throw new BadRequestException(ResponseMessages.bulkIssuance.error.attributeNumber ); } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - let attributeIndex: number = 0; - const isConflictAttribute = Object.values(fileSchemaHeader).some( - (value) => { - if (!schemaAttributes.includes(value)) { - attributeIndex++; - return true; - } - return false; - } - ); - if (isConflictAttribute) { - throw new BadRequestException( - `Schema attributes are mismatched in the file header. Please check supplied headers in the file` - ); + + const mismatchedAttributes = fileSchemaHeader.filter(value => !schemaAttributes.includes(value)); + + if (0 < mismatchedAttributes.length) { + throw new BadRequestException(ResponseMessages.bulkIssuance.error.mismatchedAttributes); } } catch (error) { - // Handle exceptions here - // You can also throw a different exception or return an error response here if needed + throw error; + } } - async validateFileData(fileData: string[]): Promise { let rowIndex: number = 0; let columnIndex: number = 0; diff --git a/libs/common/src/response-messages/index.ts b/libs/common/src/response-messages/index.ts index a4f18d252..1ef816283 100644 --- a/libs/common/src/response-messages/index.ts +++ b/libs/common/src/response-messages/index.ts @@ -271,7 +271,11 @@ export const ResponseMessages = { create: 'Issuance process successfully' }, error: { - PathNotFound: 'Path to export data not found.' + PathNotFound: 'Path to export data not found.', + emailColumn: '1st column of the file should always be email.', + attributeNumber: 'Number of supplied values is different from the number of schema attributes.', + mismatchedAttributes: 'Schema attributes are mismatched in the file header.', + fileDetailsNotFound: 'File details not found.' } } }; \ No newline at end of file From 166840dc80fa618df523f19326195a47bceedc3b Mon Sep 17 00:00:00 2001 From: bhavanakarwade Date: Mon, 20 Nov 2023 19:35:33 +0530 Subject: [PATCH 37/62] resolved sonarlint erros Signed-off-by: bhavanakarwade --- .../api-gateway/src/issuance/dtos/issuance.dto.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/apps/api-gateway/src/issuance/dtos/issuance.dto.ts b/apps/api-gateway/src/issuance/dtos/issuance.dto.ts index 6072d2964..e439b1a78 100644 --- a/apps/api-gateway/src/issuance/dtos/issuance.dto.ts +++ b/apps/api-gateway/src/issuance/dtos/issuance.dto.ts @@ -148,12 +148,6 @@ export class OutOfBandCredentialDto { export class PreviewFileDetails { - @ApiProperty({ required: false, default: 1 }) - @IsOptional() - @Type(() => Number) - @Transform(({ value }) => toNumber(value)) - pageNumber = 1; - @ApiProperty({ required: false }) @IsOptional() @Type(() => String) @@ -168,13 +162,18 @@ export class PreviewFileDetails { @ApiProperty({ required: false }) @IsOptional() @Type(() => String) - sortBy = ''; + sortValue = ''; @ApiProperty({ required: false }) @IsOptional() @Type(() => String) - sortValue = ''; + sortBy = ''; + @ApiProperty({ required: false, default: 1 }) + @IsOptional() + @Type(() => Number) + @Transform(({ value }) => toNumber(value)) + pageNumber = 1; } export class FileParameter { From 8a277ad31756fbcb4ff2deb602bcf490152e30e7 Mon Sep 17 00:00:00 2001 From: bhavanakarwade Date: Mon, 20 Nov 2023 19:42:59 +0530 Subject: [PATCH 38/62] resolved sonar lint checks Signed-off-by: bhavanakarwade --- apps/user/templates/world-record-template.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/user/templates/world-record-template.ts b/apps/user/templates/world-record-template.ts index ed51f950a..e9e68778b 100644 --- a/apps/user/templates/world-record-template.ts +++ b/apps/user/templates/world-record-template.ts @@ -1,3 +1,5 @@ +import { BadRequestException } from '@nestjs/common/exceptions'; + export class WorldRecordTemplate { public getWorldReccordTemplate(): string { @@ -9,7 +11,7 @@ export class WorldRecordTemplate { - Winner Template + World Record
@@ -23,6 +25,7 @@ export class WorldRecordTemplate { `; } catch (error) { + throw new BadRequestException(`Template not found`); } } } \ No newline at end of file From 572bd56a61a295c4a0c9361ec0c693fe20d7c23c Mon Sep 17 00:00:00 2001 From: bhavanakarwade Date: Mon, 20 Nov 2023 19:46:09 +0530 Subject: [PATCH 39/62] fixed sonarcloud issues Signed-off-by: bhavanakarwade --- apps/user/templates/world-record-template.ts | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/apps/user/templates/world-record-template.ts b/apps/user/templates/world-record-template.ts index e9e68778b..d07a062ad 100644 --- a/apps/user/templates/world-record-template.ts +++ b/apps/user/templates/world-record-template.ts @@ -1,11 +1,6 @@ -import { BadRequestException } from '@nestjs/common/exceptions'; - export class WorldRecordTemplate { - - public getWorldReccordTemplate(): string { - - try { - return ` + public getWorldReccordTemplate(): string { + return ` @@ -23,9 +18,5 @@ export class WorldRecordTemplate {
`; - - } catch (error) { - throw new BadRequestException(`Template not found`); - } - } - } \ No newline at end of file + } +} From e1ede9608c6cfdd59c4fcebda7473cf4bec87ea9 Mon Sep 17 00:00:00 2001 From: bhavanakarwade Date: Mon, 20 Nov 2023 20:17:32 +0530 Subject: [PATCH 40/62] added package lock json file Signed-off-by: bhavanakarwade --- apps/user/templates/background_image.png | Bin 0 -> 68796 bytes pnpm-lock.yaml | 101 ++++++++++++++++++++++- 2 files changed, 98 insertions(+), 3 deletions(-) create mode 100644 apps/user/templates/background_image.png diff --git a/apps/user/templates/background_image.png b/apps/user/templates/background_image.png new file mode 100644 index 0000000000000000000000000000000000000000..15b7f3be6777ec547e8c872a54aacc7fbb903b03 GIT binary patch literal 68796 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 rxz9;wHXcC)O3y} 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�^=H z{M2bAd%Vc28Qr;WiWWHAR9im1Z*6V0$nbn7S-I^A5qUJ~Vn-J`FZeao{eWMce_)Jq zKRB?iWy_Gc6yE+WGk$ZxYLI=Kr^py?!NUmdc{bi(maIWBA%!tWcW7}!p=oJ)bxR%xz`SCE&w_c=|IIAi%o>m#9quVAq)of5lhD8MHon+z8(~Ig zP3NsKkyR6CV(~Nkw6qsovJ^D@4*qTrN|7d&bx2nD;Y-|M-azu?)w!Ub!uJ!|HkduH8J zze@<$6|i3%)Z#|r$(@+eb6+6U&^03OUNzx%x8r9|;;$y4Tuw>7cp7(t#4dhL2g74d znCwP}(Yd*qV`CICNl^{W+extuXXJ9ti!tVmQ7)$FUAj!Hs6o&D?Okz(cLU4Q`|Vu_ zm#3UCA=QzC^L`)9Zngf}qt1_*p&v0NSNnAS<}}Zs)fO+*nQR&$PLAS~L@<`mDu)LN z2*sM6Uie2jv%g^%G`(>^Y6a?;XH-ctSNq;Z+0MCZ$A>|3h26oy9c07a9e--l=4wPL z?e24ATkXbHSo!>ryQwvHA{SdZtm`MU)z&2~H#p+1H&D83$GZdBs+`Vrqxt z+x$zqA&*{-juJdWL1DpxVEyFZgW0eK1|!ESnz>Ju;>tOl$I*tj0Nfw0( zZQaQENLq6j*}Uzb{pm}vZT}ShVTpJ`?{_`srC{2G&C#0AXRRT$44jF^UR)d*5&lrE z?Y>CfX!A)IFKFSLKlF3Q@}Elc_|g)b*Kp@ITGZ~MLuc5tSB)2oo;_*s zhTDVbECRtJf9?^_2`k1Y?*A4`mc;H=c7EF1HeSQiy>7Po!R;)00)gcX9sA~$&DOd* z6IyG}?U^34uBjFiKaC@PCa0=| zgFbU+sKSV4-_%+glrpoq8pd=Vr;(DhfiU080~+$Fk!ouV0_a=7hnV5Ct=-S!=ffR; z1HZR+sGyN3Cq>NTy+OrQM+p+?lcQ1oiVJ=Ez)UFh(k%W@+w8yPhR z@RQl>8YZ$S2_nY6g#zEpSPK4R!XOe)R{x!9Y3BPrrYrf&qln?5IWOwn|pEWBV97$W~!yeWK)rJy6jID>PwORy$7P_6qBx%slF-+##MSwwcyjUH~>*}_v_W4wGjV+(6LY6F^h(5U%GLfxe z>heKu;HT;7!Ea78PzE!^l>8^HrxdmQbe#~toprui_WE3iAxwIP(?SKd?N2vYON0-; zbZjbFrIlOuW!JVmY9oYSbxambeZS3f&ii$}`~3kshVsjJ)xxvbu5i#_8k%LpUhO^i z^V8jxZ`EgJ|EzI*86B)Vj?RBGiLEBHNCX8{jb_nQpI&Q6{!+}VDmG@p2Xnz{24gIs zQO(?lLz#jn-#~bM6`6W|y{M%@FnDu!!2I}L`<-a@_?!-niu%Jc=#!j9_u+bGa_Oi-#Ulqj@H-S9{13omDy~o48&p`)MJVF z2-tMd{^Mx=+*d)+MhVBV;&%;`G->A(()LG(P1;E_xkM zIFk*@bLeFc3*s06fy~Xv8!hgkpBSBn|NK&+@hX`1Sbewu7+H7yrIsN@J5oWu!q)4S z;xXIRylvzFN9B$8ukY_E_`(+afBf%d2{n{=W3_Zq*^Lc!ZzCHhVZRU1aJEqPAGDhw zr8|wunJ7@?M_+8|B2`+HuTDiSAaaT>rPH^*zM-lrO^h=PMb_s#lLLgJbPIpJ+t~Rbu0QDM>Dd;?Assf;f>i2l z3oMjM1ZZgY;dWG=1EglS4Xm3jeI6a}nW4E?tQidr@G*=LHsQ@hd9jloZ<<8{y5zQe zF|Ja6LWmo&eUKLzjY1c6*KOT$aU%QJ=Bfvc-@x>k zh6V^I#4DxGf4nW2ef&~?oO69W(Lm<=aS!V7-Hv-g>+b!wf^)s@Fj+WTsaVeKU*yc` zP!TTw{d8$H4pg_lp_8J`z~;RjT2I@Qjr#9FP_8`T!pw{ykMMk@_f*u$G&0+6M(=vn zRclZM)lf*(@ol$XoRVMhAyz9(mJuapBf_Dlfm?s+WUg^cMA_5sT->VeTau&lz{vzk zuekd~>oC9HWpYb?zFMp*YyI8DNUQZ>2OM=2<}Gn+A=N~>>Gn@>{wNUq+;&)U_K2!-`xK>GJAtb=x zpRRw@mfaQ3P+f)nRq!}6+|BOI^9gt4wbEE4OB=m?tIc-(W3ph(sin&ve8Q!Sflc{P z(>XKbucx7mIGL4=-VP298)w_`uTk657aAwFdHhR;OR`!PZR&L?b`2&I^@nrh4Ixs) zWKLUc+_sB___>ya2hRDN!kL3jDR3Ue?>_Ax1U47+67}k~(=3!6MfK=~uBvO*YLJdQ z5~mZzk=Ja6E5+17#BRtl`HPG{Klsx*e4)%()stPC0b4z>8V)7>M)do#k`D&XDB?Up znW4MOH{uZf5kG#`o+0uyp37g&IeZMYEx_n$$X&_4 z$=$_2-ta$P2eli~Io=jf8BP~rh;;Tiv&kPZ*i$Uie!=cEe!@f%YFr=tl9V;UX0`&O z?Y(SinY6R{>>1|7bhR_%Q*N&Z0vBN;q`{c>0=`{&AqVAXQd54n@-x&rL%}~&b6xfq z=EfF{a;dnS$&zpY0|JrAzcABUw{f!(Q_FfzC}N*QT7Z{R)>wKOWK91c)3pMJB*yZg zbDpbV+~CV>RJ?3ktF`DBR}W`ZRPoKUDIIrTsTWjqX62*P1u6NIZ^@bP^YoW~5;OR# zw5ZLZGgBBdGv^3=r<%KDEd3x{q&Q8r%Yh~ti7jU@@YDdWEffX+8rQ{jxpsOoXq5+t zo>y7!1B;*Q=3=_OH?uN1jY6(UU|?Vtf(*8v*HC*&=?f(|yoy7*||;A`N_@VGlG3fD=zryZO)RL-&^%xzx^{+t~)W@HpkiSq=N&Rtxq2b- zTlYSkc9tzz=)Ts!rHA&3-<<^MwmruK%JFSo_Hao>(i3~Z&Ak;HyN->aV3lAap1f|4 z#068-fkbSlysDaKlTm8~O-Ev^&G~ZH;SJ00!fW@LL>pSF)oP*)xID)q7;b}TwY0AL z&-Gewfh@!K!q<>#l#uUVG@%m?Vdfy8wXnFMs2S*ou0FE)Fd^9R--O*VUu-14$t~g^ zZ@=Y_R+W!Dr{}HJud_Gx8Gc|gbH^wp;Yd-Jc{oaD>9WVmNkbh%HKhIVRulPi3oLEp z1Esni$5<+D@?y#=ldQ?t2wJ5OfYG0+p zP+N0xabfI`X+LozOY37eS7Qq)OfaFSOL-0Evv6B5X%4SyC*yjaE|to(U_;Kcs*h>J z)?(w#+1zrG8R;|5*gKeKkao!lv?Zvjok)6`mK4gSs0kg!@vAOt`NRLuCbVm zy=-H9f@^B-r~z?l%UBg3leDoSCX`8#rmtryC(_F8fz3Uw`1(o1G~1PlMm> z59eJtm3#C`V9h#TQ^@|>61W;g{mMb2gcu&lW7ielOnMY`f8uU)EqdBZda4iae@}}f z1n&BR*@F6H5uWaPx0_-yQVAy{6geB+*Wu%MwKIJeTrDzlcLPv^LMor$;J4(yh<RjLc>F71 zrrz&nIOEP^pK9}5^v2NW_HVy)~wMbM_ zP>gA=)bAzZI~<}%;bmr1m##ijljFX1PvEAW2yYJ584>LgJ}CPakW1QL$!cKdwUMk! zX)&B!Y1&8U#goj0_9W3IyM*B9W#&c`pab&Zz^RhT?)Hu_Kd2M*@GiL zGOUwr_;{6elQ1*$^DQQ4d4oSx_@`MLObaxxF9#A-cwgY*ZG?nC3WEFZM9f%S`?b_r z7IezV?%)Pq$GaKS-Uy2CNBk5Mrg+p3Stfh)}nD zeIP@nZ78GRiIhkPjgP~tG`lq3d}cxR)@<4aqS^)4krE9SoN9;4Cs$u96gX8DI?&vo z-@w#+k8@N z_D{`Jw|BR9A5;@LXrmJuOQi>0%*O{wWj=J} z=*rWL78ADo&uwlmhKrSWXe(UYPR8Zguh%2_-7YTTZ*QrhM6C?R|4@t7HB?w*{gT^6 zkxx~8CF0r}ljiFiYMg-h?$5jn(Fmy#>x3j_*9S|R>3W{mw~I>?lht{y<|{!K%OvAHXBUMN z`R}aL`F!)`25rbo@r?+QN)N8rm$%(TnQ#xD%L^I=2h}W0m%o7~lY+>Ja=*!u|5TCj zd&9-XqMAm7*{W)n!rgr7jocR8AL}Om-L^=dS^S%j%D0(hRlIhy zO`!YPjP2xi=E)k%Yr&;B&#FV25{hJ$=O>|WLw^N5_I8d-nMhvj{PVdyx2NKXK`IYZ z2X~oVNv-3`@3U_;j=rbJjONp*@KsCNt37?O<+}bci3WcX6b!Y!0KJ^@*N@_+jsGO^ zd%olRYZ=D*-sEirC2weJ=QKSFuH5^d*yBR7V#`cZxBCZ&(UT82WK1ID99wC{{OUkX zlU!fFnd}K97O*j&kVDZ_S&^{$N5YH(YFyE8^933li;Ee%6fhV5HD(p#G^V<0eX`<7 zc{f)}h>t5t>%bn|a8GzpgyQ4ZXt$uPp^^A)Rjzn6YrFH)ff5EG{>rJxtMuahF4aZ! zQ8X;pBISg2?hMJ>`H#Qb zH4>L~bMwsWV(@6ECxO~ep9NTt4VPE1oc<2RM}rJ6Wp7|m?A2-HBq=igDZSTzD!65v z%#GCy$DIy*M)40J1~*tz5?k2VK6sdDpw8q_c<-fpX=>kc9bAnOw?l0x`%FhAeV>vP zPdqdA5sVVqWSWkmT%F`h&rpE%d`;vL5o8LrL&oi8_;k)jr!xHFd;@SiG)S2t3jTi= z6{wV7X1FY1gPzfW*vcV9fwp}6u+T>Rwq30Is|_Ij+H9}Ef|v~7iAECQbNcZ!UNzCRIaj%vyCI-k!#$`Ws$e%MOCjc#QPo%kvY#vAQuXDkIH9dkr zmoIpJjs#knjm!?w9e@#|GI{kjoTjToU0S~iy;KNggiN&@o87}yJed~pwzcD4Q?9v`$*~#Fb#s|y@2_MZZ5Nv{MrfCk6hF0J z;XFcrQZv|R{cY&xfp{`SVSoRpVSW0$WQ$0{CVpF~Gec<<>8XT`;l53yUqC#j# zOdT(UATOi_rjb7*W*b@uMBUbchdFCI)J7TLs^_6;L$iOS6d}kVp`I{O{12Uj;WH|o z$Ko`%RTQOEj_lX)MHmn*t=CQPxDUQ&M~86yOLpOJN|Egp)M>A?%<)dDDykXj8L-=! zLVrvas=2P4Ms}-de9-xT>#jrF4LQ*ASgx>^3QW%kVh|?6iZ^kJGo1J5J@?pDdk_{b z9elf;os6abV%_z9ms;KO(pLN!?q7q~CJfQ}Q$S3Yj!70nYq!zK&)>a; zaVX?K*!rHlmPmb;M2DN_9)3i){+aGcrq*d=DDtSX=w;I`l(qm#<4l$0TA0!ycM*gQ zJ!v~Nf|=e#+IP`Y`o6q2no0N>BM)#(^R-&Gf9GF>T^)84CFJwn`8a8mykP)Be_hM3 zk0tdeY9RlIZ#77B&0_a#a@xh8#@R|rPPa=m>u3i&CXz%UI-`v7DZ<4N>{=B1I+U|v zI-DlNFCcK$)*iTUFFN!IQGTnA5W9bVh~#|r@z0MfOPbXE)z#VcjnZ0UwsH%x^E!Ja zOzJ$v4Oz(V^#~PF=gFzr^|h$EsE5l^KD+?GJsdv^!uEP(Pu3Bv7;2w$(xd43@t3g{ z>=J%kTu`I_oVcsI>(gxyl(dcDH2i`rTUjX?9`+F(QDk~v?@{BhFDXbSHH`*y2++>` zSyITCte~#`^6>W4DK<9Z1u&^CGXb>|R9VHW2z}_fLBP9!Mcy`uPoL)CnQWvA)4yWy zuoYwGph&-mpdbaKa!rt}@D&ZyVE><906sn*%J>nZiQ@Vvn-cH-^66rlw0!M)E>>F1 zh8?zD1)+V-#mJjN%Xyfsf)bCbj5lFL&+v#2Tbu|_f({dn4%?hdf+uD8i^`j?!=JU4 z-@bY#7eF6~BmF)??$dj`XB|WkQ4qQnK6WyCvNE0o3=iVML&2qfF(xnnzzVjpvm=+K zY?w6NDRpEI{I|p*nv^n8$o5dS220hXC_P?eKh*^1k65zuIz6V>XlU(VRx~GG2NbLY zOTKV(Q_#6(`7&nJS5s!Fu*T(CwK<#o?)-cHZI9e6a$~C-v>@TJDIDG z1+USU*>b?1T=gmKbs#21Y z=@z~?n9u+FHmtBW4s<fI|Kmc=iZhMCWVE+3kG>keK>gkAGSGZ=PWqvDxqAqUH}9=So5PK{aI4Bw zq`o!i&LboWMTe9p@;E8JlEgS|ZW{|xno1VFD!e$-rG3D91COK1^VC8zmWSG#FdvTp z;ohc4oedcaeCfXa4rZlL zfMR3_FS$21CC`g7j+-$ydKDs3xP9g92dBh|lzX6`Be$Om>AGe3W*tzl>fnlvtZ!Zf3uy z$Hm~O$t*vHh2X0e3=UDz1&$H9CpR6L%?8wG7LtDWX-J8`T2Yvlb!LDd6yxmc4+v|A z#YyN3F7B@rwY*3(Wi?bJsL4?=^^*qiANdo_7n4Synx@=cn}o&c21RH z;Ai}~T`>+O`KSpE4g7LX8uz7W|(eModhs7x}kixMp1@(LUV5?pf_gcsOrD$Zcy zg@)ci9*l|?SfJ)_lr)AtX8G!wBz-&PqyEfTJO+{gI&_I`HR52DJ2Fft8GSnv8z&fo zOo@MS)8VnawSTREEyvJaW%1JRA$W$xd04hua_|F=8eMR4ptj6d2vXz1gEGIqfgtP#_lF^i@A}uKtVoFIZ*bCI zea89X@9)2i5u^;EgNK6&e0cD9NPv@wf`Zci02g>J@aQXJV~B(2C_s`4LPbRlcuWHP zCU}}Mf#3dr`zIve(k$^|U8{@i(qh2^Bznnm z{ct$Z!0NZ)KFE7_K0dylC~AXB^GJPrCRWzned2TZ|GmWMNbk^4?$}mo(+MtcB>q6$ z!=S;X&y?%jt^S7ge;cr+WLZ565TMuf$@&KCbK*XrnAFtN^jLmU>3nYVmp@I zMS`1q+qKdWqM;`I2*aPQx3ae;8lVxdL4JYlhHu`!jSLIhaZDRB=jY=SJ)e4;tHPD+ zgSflSPj{yMqzobP0^3LDq``(e7XJKsa(rAXp97&+dO@8tq$1oCdA0|uZ7#87IK#6GyWow-S*f3B;W{_>@_wMjZlIpb!R z1UE>T>Fn(6qm`BKe}CZSUey2d=g&YK<^qN-`m8{0CtEt)#>S?w6y8rzV%Hm_J43LWCao_Ia*$Sy*hslPA-J1Vu#y+1S`< z(-i24oi zzXHRqi4a5=agA>Xq8Lg^Nr9m2>uaSvXlHk~UbiGZKEBWNmG8}B^{mts$Rd@tr#V~NEKmToBqOMd;Ft#;o0 z67!5vDS14c^g93Bw{M?=gGKk)L4Yq*Q@V@^% zADV4&O$`d_iWZ|G#SLoKD?w4b+va|}dNHDPheHv3<+BKc>=HN(6m!M~aenCO^}vNC z86-pzDEWRO@1DkZ{aaq&zTS?amL;Ga9_q)%A%wSd!6pBEJdbBhY-~hc`uCakub<@K zs%U6TJ$O8sAItg&0T5jc1S)OEJE ztssa*`rOMmT9yP?T4fv`6qMHrqoN2{S^rKHNFX=HbLD{q5iSnSLWNm)b91xiOT=+c z&f=oz;^JbnUKNVGr*v048Wx`qVkVec%voUXa;nteq&wB+Rdedu)ZyLTL~0*@o{WnNT+&o)i@*_0S64*_ zKF1i4_;cd;Z{J*8UDr1@f`fu==j)DXNk2S|xyw28{l2lWQM;&(Be8t6@(h7MK>h&% z0nOf*uVs58Xha%n<@up8OOnDu{>_n;__(y@t4WPo`z6t*Tuw)Zw8;v=jN<1v8#gKc zemrQsbMYE#xU?AdoDrhCvVTFACNtM$rRw8zSILf4UAfqw8%QkOb1W|`1U{z!ZeViP zb>R?f`@vExp>&2%3qeO&#u8DGWR#K8ciD_Q+EjlhC#S!~-j5+7a`LO&!&apf9vnhE zF2B3a6Zvq9${7esQBtEo6fn*&xJWPb#)p6hvf;S z+PUdz2#Sy~v9hv~k+~U8Gtkn}5z|U156)t`DJ$KUmdT$mwa9;A(8_82uP&$cF5tE` zRUvPBS`!N^1pLjTqc2U3{MNWd^CBY8p5|=3Y~*nN**#x~lm?A!eL(-+V%Vo<(u2~} z%&g_^MEF#eM4CglPFY`HT}`dYbyt0!Ij5&YFK6s%SZJ3)DSdTi<+$@uuhQIBUA?CI zyzTqhr3uE@*w~Ys-BL8DskwP=jRVLl-jb1#L6B&M{Q*;vMk!*ia>T~gme!}KvXYyb zxvxJAVF(IKu=knmo!te<&M{@WmP|X z`0)OHOnN#4acBa&9-sZ9Fb79zzWToYI*`y}W@Z+0Kj=)2I++RhH*m-QLPm}QlZ z=+Yf0+kQfy>CL~mNN_m2H7x0|zxE+b3T25%piI!*d6lbOte!uyKV826ug@SuAg)CL z{sDUWK2oI+c54h4RNCB3tpS!qL|uhZ)vGpT0n0bg}$Q-UOgxRUcGC^F=m)4%`y ztIo<=bkGV`aWkdwT?`H;*a_O@U6)rr>K`!!GCfcA6Z(EAU4SmxlQCIZK_Q_In2iZH zOpofO2(`|eAHFsE6(}}>C?FBxK@N|Da%gdL83k%fH*O(Xz()0YcHycB# zIkM3$9>=S|Du9tT0-{TiAr5E~Ztm{~(!Ke(GXw%*M1%q(V?NEHR}vZ|I$l~>Xa!EH zt1G}RDWblwLOQyNmF~YaH3@^Zbef+e(CGB^)hUo82JM5bNI#IhDWs5yxbCR=(2UN` z-e7Mw$(C79X*?mYSnrR2%&wcnXFIFP+r8bh!!r!-uVD zf~wjo4TdWNM)VzqTr*1}ppJHKu($Z`rbs^}Dak{K(YEWP){HB;xmm;<#>c})87adO zul*tDt(jBS=X*49n)_J!kmsx+VJX|4# zr?8=6=U-pk+qe8R_Dla6S51?Ky87^U!L=y^IB02B0u2zpzV`qGrljfs>r^Q;t(>u* zD(QmlaYRa+cN#m&%F3FVnfctA)$7(x%;N?hZ9x+jqKj3bzf?rR znO-S9j+fit6`H<#C_xnS=Fj{~#Cl@J<^Ei>0T20^$ZVuc4rj6gc`ytw5HTEtE&DVE zKvh;&R&)s-%E-7lKqgTEI3?=1+L^DBD*9SX6_Uw0E-H1io3FD1T7uvexO+bDY;Aps zZ0=om>WR*Px$V`(vucz8Xfty?Qtl)9yZZCzPJqJ_4bt83t~ePP8EI)Vy21@|C$>>P zEpsdd(S1?OpZw_h;zbwmvQG5@l35-e9AV=$6g>hFu zqI#g)lE=->P5r}%;2?Z&?QGTj32Gs)Kfn8TfFkB@$vZQynd%~quIlPQv|yX*ni(?( zIw-TUh1`F8gD2+U!$!CLurFVfnMhueSq~%(#W6_J3ZC4Tn~2Fu%Ni)X@wei6z)+c; zjT*cHWY;B#{)~@TnD)gE6BM=^T8@3B6*<^`pr)ZgNWs;47o(WZog5+J-J(@{px0D? z=H+W^s~4TW+I|EJYMahjNJHBxCGo2HFcUfxJ;mCJrS~{ zBqj{C%~V>vd47^=-y-O?H+}FB<+#sDJ{))ys$e4>ow?=Z%xo~m`PZ+bBL@gd91wEb zla1?hIa+qVI@&Y>;20brfbzS$G06sI$HrntHUT#O+_NUKJ9~~1Oh`EOkr(_&9?GGq zsYJ;vK$BQ>eu;rI-=jbPbw}%ykM#GGgzFrwdwm~BIYA%PSf0pNH<6zSum_UCM-a_BK9baA-A8b4q$lEepZM=jT! zkNNg_Nv`*$HsTrKP1S8+i%PB|JSnxy=W8tTB=m z@>I@s_sq&QiO`uz!g@TV!H;w%%)93O-3$Ws3RMc>yzE`0El`HK#xC-Re zEXvOAw$?pl{kKWPxbq8*`?gE)b!tgT2^p?!t<4POqeu6GKS+H6t{goUu1gV=#YC2DBT#5?rAA3>?{i z*I%#*X?!$|V+za{o35kDjp`e{U1(GKOe@%QYMwJN=;FgUl*Q}9LET}e|>sat6YhGoO> zgq-TgkV3a-=a=xqRwOS)BrI81UwdJ3k&>KAx85l}W64m>)!yW8XR-(dA`Uf0BG5cN z`3b3Sy88P1y1N-*u%o4^)#QL|OD(Op?rf8zqv8y4jnORq0|R92KI^-s?lEsng^iy< z{yl5-*sS7!yY8X*4B|> zl>2LIkI2a_O-xS94y#*Q76HyZ49Zu^xHvuaX31$49R`uzZ?!*=m1 zRJRZh7jKV~p{sVS@XtrU1UOs30rFm7Tx<^^@C~8s1t)rDL29bI+k7G~QZ6^5rxFEH z*d{FZhv9KkzFUAN0dm;S&u_uk4}gC4Uz*!nTTF3%4Mn&FA!#u&7h89{ltanf7VP>F z)n#Q@h+C=j^mIb`nc3Nt1h=BfO6!&1K?}9`=LR=%s1Srl!S(Na%3!I+b}q11^JUS- zhB*WU8_7n~)O(#%LWXL1u?hv?ngPMVnAO_O$E!j0&IXv!^4bKAl_gI~ zyW!T}9!YqYR4^V60Zjww*5kHN%nY_PbajEkB@^iyLQR%mg6v+ z=?2^nYpk!|pQ|+}{8r8Hn#=Hl|BJM0{-BS^H7+FX<#jX3sv#*Qg{<>fS-4yZp|hMj zUNc)?*Dn71cL@K^UTDp{>gwtW;{jR36AIULx$$SXGPx>_+e-#*u!xaOclT<*`6=e7 z3h8KRCD+yxvd4J8+{UM+LCo31SAItuX*xQ_=-U&8zUE4P38b*RugsT#YHiG$vktTSv#q z`EYOcoEOS$ZoY5(gcir}j*`>(vs`XPRTaS1rd-MGhIkaKt$;N8{{1_+b2ie~XLYfc zIBl~F64r4`9&78rK#q5Qejc&SfAan9n>PYZ|9Zv3P!87CPRm9?i!Cps;`c96Q8a>1 zrrzGStfhmI>9CoYp@hDwmKKBurD%o{LtIZJRju0)<@_=X0D?d)!dF5<;9ZHK%rVQs zfi3{xg}pEKercgO7ZZ2DsHlwGF+h5n^!@wia2pcoQeDsm}mQYdkCjRj>LfnxVXf`B@~ei92_I1Ewf$gPPYiJKC$d2 zCJ3(sAXai>;tHFY3gEVDm-N9_{REs@%A=~98W~wxs>p5t@Ob(7{I1s@gLdaE5x zI(3d~H|ME3H9mipHE6xBSK*6Z1Aiya`{P*!1qEX-F2+o{drkG~Je=IzXvExX&i?gz zgOvd+9M>Jqk9G2rmDPJYHxcvW;}3;uHZ`v96hnrv!E_T4^duzO37z}68M(>v9334a zBk_f4N{iodfjllGxmAHdRbL;tFm*@Av44H432Zu1hZx>JU3XZ|s4yUjQK0Jlc>r59 z;}R@YuhGr+e5W>%U2kJ+i;k>zzByGke?nJRmqYJntv4p;ic1erOdwsx`p7h8AqOHB zNONfZ9s(mcS&NosR7&?cn6C%(QDHVaQDfSOA7?~ z_CPH45?u;%@;c)#+{&*3be5x;5|BRt)Gz4i+ZPx0vy{)?MP~w|k1^>!OUeNZg&&|b z(rFGTBQKgSm+~jJSrc38O}DwM`E+Kd1-M={$S!}S7IFh;^hZs@N07!Y`CPB(ey-yk zNJtZK4I5Yo6vN$d5ADau4Y|Geb1ME}gR*6iT>y5D7&izkcAKVvQJ8CdVa|I&$KHq} zOp6Zi*ndIlol1-KT)UXeA4!oF*Jn!3@Sf@0>gwv*8ILf-v_U`ykpswT0fVGfX?}A{ z=sA7E7M2aH1~4dQX1Ogbe!v*3%C#>4?2b=u@#8{}p*?a_`9TK^*vF?f^Y#EwdU|@` zUqwOM9Q#6EnS_m%mG_nxWTZLe@Yjv|*1FAI=<}!c>LE-0c-&XhAiCm zRhlGy;+t8Jv$3$Ug3%r07FcEOhl^=2BcJGSuVzF}w3wTd3%Ez&EhQD0nD`ho6Q7EY z94{DviNNt=tp*WDXv#GMehD#t4rAQBSk!BT;%D{zeL-6NyDL1y=-!(OdxyCk03`iV zz#q?msvZyvr}cTx!9gfSz6p*B1_nT7txZ-F>To2zhn)y9@CX1>fO+>+RH-12B^vm7 zirXDu%Y(4k);Bz?Ut#KKXn6FES=GeY7!aSh(AD{r>eVIiy>q@zr4!qjJM;U=0Y;iq zQfmfwF^yj5AARoP?OPYzL3|w?Je$ZLAtNQ#Yxdd)cew+p9I3k%F<4IIwh^)HXMb(!t z48>%S!_AqMPgM(G9;Q`Wi}kAe`C(<5nUI0umL2bJD{E_2mUxf4)bQ2}?bD1)?{ATk z46?$@q#|F-YHAV&NJYr33e|q5rTNOsvo6X50|3Pw9^V@mV2!WR0E!bWR0E0N*-lX@ zpwScJKZqt3_nERVGY|Ln@+3!OWf_61MRrR|5AXkg{GE=82`E!4MhEFus!wgZpPQIv9ZRFA8VjXSn_oLE!LsHlgXgo7Q+Ld_#i}DK6e}~1nzKt z=Hl+&1KaDoY+RbVq5z51*u&sH*=WCNvw?gC1`!dFdl{8jWjY6d(>FB^U+DMckdRUf zI(5#|r=_J$`5n40}rKNk6QA8B34mh;S7kh zZAWk6WN_3$R@Zj z1BnB}^!9EnEv1E^4p_0e6hqu?+8Gv<$u#SGRSyoi``;33F~2)PvAeGcUbYwWU5onq zkblLhaf`p0&2-X2BNiw5e>)9OItYND4hhq3}aQK2o)?ay>D)84ql8fsEf*VV0Kq~BRqY3(KO(XU>;qQj=#+0;-~J$?Gc57ek% z8&}q224*#e$~-3qNq~N_x;G$I0M!YO@Z8)S$Re-WdmQ1Fphgw`cw{8ye0*opdT;-> zpmpZdB9qzw;==88$*VbCE?4AO;suAm?o=sqIgk(#_44xJ`}@>eE;qpCz9bF>(+|9N z{(w`y+lv1cwF4G#w+$vP@7@-+%{FathU_e3qr%EdvG@!ajS=EKf!!e}a7pi%Ic@d!%&Ud{V1@0pN4b5w<4%nXN!ot4x_LahaRv=YQWT!w! zM`B|K94SXE2zneXr|?<_6VlfB-D3gZ>?=XA?-7Q_g8@NFyw(^0mC`_fzHpPcq4@A2 z{^|RDP@#O5FobjSzmbbrivG$HvB*^@vad z;Qq=?KGQ~|=+PaJ8icOopO%JJt}7{ilsy(g3I(U&ee}i_%q`r{c#tG zxhX*%Eo`7X)R%r(CHs6D1^W1hAiZi;t5urK)_ySha3Jjvrya%;$H{cJzIh>;qxd1! z6-E39$`mOlHr>H9ve<&=1~D`#9XamFv`Ai}tCpJ@OccCZnN^pH-+pb_(sJ3aRA)9rM$uEdOj*flf1Y(6ZG5160fr*|V589`T_2 ziq+VTr^)j@NtbmPZT4qmAG<%y~4Yal?0a|&T3e-bc!lcMqXaNNh60TthWA|{1+DYo?gQx zKIA`U`Pt~|VNfUBdZYFDRZ1&{^VH6<2k1KntB9B5l;%@ z+}Jo<^JU(|w%GkyoMcun0Ba{Fl0^Ri7|l~=lFR~=su>v=dQDFNSO;k!FF*h0#s;V( zX=`d`GNpWVCCbqfpeg1;`HbUP*k_z=zyxaz>_G+tU8C4rrKKfL$`3i$AbSRLG(R6eb|(X zn~ST{xI8sg$IHtLK*8nqKqGR{VF0R#9-5@}x^7oTMnRIv2?-$GdXql^oHr=X+t{Fk zeBd|iS3$v>oH5XL0oW`ao<9%JFWo$h$P>PO`=L_E#SL;jhLDbW&(l1Rn}oiW%T-|_ zVPIhR`{$2`mlr6hfsMjJ4~U3}*xC6A>X)EI+pO;nlC!e1vj5ec&H&HatWVf+biCHH zFhBqD<;!$rlwG6@_2b8&egGfNp~e=q~#-WRAR;v)5%0=~y{Sqdvh7=?t5?S+a=Bc*yptm%BflQR#l~XdQ=&^K>*+0k`~q~2^qM+>Dv`7_uuu`Fe_sG3 z9ZKbw&Q3Wya|wMtJ~Q+GwfEisShwN(Z+i<_k?fhhGs10TkH|`PLiWy{*(*sXGRusl z>@9@skdeJ2Gnx6Ep6~1X>*p`{+`m0fxbORVU*~xq=W!h8bpbPni-4j6DUms)r?XRW zYimnWQxk!*eKPw$I#-)?wFXlV@q(3+Q9*zx&A6OWxX3JUno+I@mE9$0SpYdShQ z@8NI~`0N0TU~_{I}& z3>;11-ipO*74qXG#WW zBq<>Qtz}o-HTSI_)j)Xei)X+VO;^8>2ye+rt_r(HM`s?u#3cpB&<%a#TlI7a#~;E{ zSuhOCN{*l1{Bii#pNr!GKS0-@)@1Nogr=sB0Uxxov_wuxDH?EcKp!LpvP$c`3)m&# z-a3e&=*Q5gd|BOxHx{z)PAV*9BaXCk?Duka&%0Zc?f*fp;O_1*IvL)VOlfFwCAUij znn|Bpyw-Gn^a#Y836+%Gi_@PM3yb~9%y%?j@&hH2n4!*w1gjQEcx`t58o#>x*3!xf zgafI^-+}Wf+1vZj*B1wm4;wrdPoK8&Pk;X&L*IwAR{*~y%V6u|1X{zkFjs zHMMnh9~c}dYvo`RBdD-3w=|~#Q8F_#hlPe(TC#zCXwK^5>C+RS2p6n^ZX_BQ81&A& zD6^0~%>fY=J%m9@3Uohsb`%s8PkephV?C~>d96uBPOiX~0?D)ac!gH#=KcHkqw^il zj$ErAHG9Cv1T<-C_l3PMhy$ERkco10l^}8@e{2DPuBi+?2-pG$>1aZ8sS)Z_)nsKB zOII})7m_T(`fbG+hNvf*;xSAlnCW@pmJ^LVZ`9e`z2Tzwf?8M{1gVO~2z@h@*9G(u zie&WrceZ3@8d_TL9L-`t-vptPxwOQBv;aHs-mKX|nC2!IS6^8DY;IXu{}>FdWVX-O z)YVy1>qGW|yk}zKkd*;9V!qAKH)#%}yTEPNwz85(wTbLAM)`<983m^mM2;c1{+&Br zQKZZuzd1WOeHp2k+=RzhZd_dqptZ9TY$79aa(|o9LHOzGBLq2yQnKbD20|!D0&A0k zH(6;_FU#=eZjpe10L=c@d#pdp%mg)%WK{B87eG0=U_&%ui;@zk6~OcG1&$95;a&Oq ze#jQ8!lQw#PMfO!7uTqt>gwuxZ=I-er7jrwNc}$DzMx8EkNGPcuY}Q_4|LAT%B(;$ zxM0aU5d@V(qL>W)H+}@9yC>%-f9ysI7b75J!`*Xg#>RD;Z!r+{?AzyMpJ#zEs;aKO z8g0K@<&7XvB#+{9;1Po9u&xe2h|lT46Q};8m$D>Kq@dVBcHK#gkB^51Wd5D)&Y7a( zJo8muRkcm##eBq5zKZMUte(bd3(}f)~SH{8htRBfg%ZK;6MP0JOhMLG@74JI~Q~%Q8Yd7 z=nf1|imoqwYX0!yE@3->4sZk(6cmJowS&kJfwFkdhfv2WHJIA)6E3>dqH>Z3Ru@Cx z6c?*PZ1^--bp?V^3PJLQ^o)#J&G~on1Z#d0tFw)Cb#;x8Q-!_>4fV$k`(|mB8JSD{ zR0GVBU`U*44hoW!LrXnb9gS3Hv!HhR_uB}C8w$S#I#6z&6%%uNrzOx|bHGyU@9lxO z<0N|jFZN3lUa_DSzvNc)h6~bA2FN4|!-wEDgI$a4tQv`N#Q6I8L2%GsTIqNTHP~1f z7+QLI)*-F<^g=WWQWFA)czzQ9+gZfGpiCbFf$gZwqN=7Q6q~=a1keo5A%PJCgh5cy z4J2GDKGREJ)nn~jQGMj!1UncPb7m=!p61`8?4`!uW-Vw?W{idK(-1*^1qQn_!UuRq zlXAEj6ZFX=DT?+%@L53+gJo69)^ckWmm_9@j2>xOb_+V)5+fjpU0AU6SM@meT{yQ1 zK|o=vsjr7-3gnLrdOQN!7fCLTK+;031BVT8PfW>Cmw4$peAcs<%xCm9&FBZ}QyWDi zSI7}3&*-?g>oS4 z)B)tF_I@hB5cmN7=Ui_$?^06QUnsrsav=?d)~i)i)(49rnVNa6lbdH zCDa(RuZ9NC2c-4()&1RLXGO*1k9H$x=jWh~>KGe?ex_r6uqMFE`z<;;8bL7SToy5I zN9-LQeqZA-XN6q=y{$R7SKdLqcC1?-IUeNiUar^2Y)AtX28+)wriHUDW z`+BIV;@7;V1y$jZFpW#&@b~YcV26YITmimED=?iEaKy}E^Z>wCR8j(&>alRf+}zyd zA&0Z)LZ?lB1ZyF2*6CIj9od92~}90O-}=}rlT_ioqStcTWBb{ z^Uo$Cq-AX5NA-3`SJz?_wOcLW0DgPL?*JQ6GC&6}D&j;C>@YNB(7qZlA>?xK1zHL6 zKuJar`)d49B$T1NJnV3DvL3v@xw%YIkpKu*R;k1#CEdlZ=L}LJ;ETKsa$fXRb}sV0 zj0~D-uSkY>CcKG>iP;jK8~gU3-pb1uVwM*b>2OgTktHeR4cS+N@sBIj*T<)$Lw(*w zyt=wtj|(f*yx{Kg;vyY6`K#1aR+1eIbabc-JM7@SGBBXp->Idt;HM4=34yrY+9G31u>haU{rd#%6|u38A3S(% zTvm9O^UoimBh_mClCt9Bw#4J*T_4c(fLmrr=>ecgjr~$x34G|0?WDMv04;DW-8|{S zbKhyc#X(@PKz~bqr^HfhXKUNiDIKo_q!A#$*r=!q`1ZhgtJcTFLJc!kO1O^K?xLYV z6vG4z{=AW-hzTL%0GrQX!^Mye_iE-oNb_{8Rd>0pQCAO|!J%|9(IEl2?koq#pK zbPr8HbF&Pl91w+7RU=bVw2C?fcLCQnHZ*`n7ZA{>lm}L4U@Qd$QsR}YAb}FL!z74m zL2+Z_18&Nx$w@HdKCb@p>656RpI@b+iJ95Y(NXHFLf6Dj!At@iv}>&iRPez;@LtZ( z&4F3)-S{sc)lGN}ILQC}`D0x6De+Zus0Fp@4S;U`@dkNAV3g+&6olT0LQ%(mH(H(T zW-1q0J*=#l$;N=e=TD~uvnoh-z%hDY!*>Dx2{vM*>Gtxn= z!SYNLgakk|{M4}G6AqtHIUg%4b+}To!&=A34H!tQZESEOP@DoPWu!7WR^Z!z;k#lWRX>&+-(_*936Ty$8y%NXVrg{cPsF z0gHpyT7?>$^f?XRA3Q#G=xUaUuA_e$+{b|XEF}dFR&j%tty#n5-aW8Q?9F*5D?f); z{NBBY%!Ybq%2;S1>db)bzIN>zq=c25&?HJqPDwf9z^B-NK0Zi4r72{{R$WW0Yf27a z6lL7W<;xdAVsas*SL$E{W@%*wh&bpq=kfju=P(e?Rsjp0=p}Wg~M;Y^+~z z#eSTvt?fIA{jxHOXa(2-fR_Lanw&%70zF2*WPi^Mz~_IX5s-9kn8RvY``79HBT$T8 z8c)Q5oP*dwf;HcQtIyBto?q6S<`)>Duo*xXa;udBZW5%g%F20Xq1OIp>6UYE0s;aE z0zaR>7zmW*-7zd7_=eIaUf^x^J!!1=eg&)mP>-MgY%CtIW63fl@9poue)Vc|YYPpz zTSVT@-)Nf15M@+2SMmr6uB-E{tifvMKTEUXr*=_^%zbysyV&`3+xo{V^QPr-d|h?} zOdZPL+c+yF0AIL0V0~x%-@*D%2$!8smFZ$AfELcqX4={i(Fk&MagOeP1@pGi?C0(K zCSLL+aK@kzB3m-XYt}^m-Vb_lBO^OyWo3YWJ=Va=wA`ooobT!7g^qw92?jh|)k^Lb zfjBAouYds;y2i_I`2GvDySBoG1v71Cu0hL_6N^+6iG#oXK(~Ih28IgYP&mkA=mW*W zEx=Aa4lIHIp=97$O=RR-Xkg1>_T>jiYOv)=FejUXno&8>q4r{c)$npYqP{nSR_s}y zlQEcQ$~52|!Tb!k(8UF01F+U({yn`BlY6eynI#jHpw5Pag*DGrcKR;1)qIX!O_xJ& zp9fYmmt)rPxhm_d3AEkxbO1k6dLW0Hz89k=WFg}OKX+c9Jfuh!6}*^S!E{1JorYfv zk0!vz$Q%s;$UF`0B25Z|M211@Nn*3 z&IcwY8f?0>!7at;1Xxc;%7c$%zSIxzI1;``NQ+5WKvm4TgGp6W`ft z{r;BsSl=xfvvjSBh)@LHF`jljGa~~iTlgxVCC$_y6Jj$12?;oEAZtdofXrLgylN77 zl8UM-NifXG$u-5tgQeW~{Wx6C4UPPxhYw-iA-3P<^XJdBgxJ(igU)u}sk0eyEP-so zLIy?znawRaH+Od{D=T0NPTEL2Fx;7`6MDkKBO|Ze*z{ZdM(6D%oU%VabO8D7ZWD`t(C$q830kSZ@ST)XCANrlEO}oZPzX771OewK5Wc z0=(mYI~&Nr<1?_uR{f<8Wq?YqshI%!v}(a{PtVZQR3tP})O|VfVSuP@h3kNS&g^gX zQ}OU1{KvdaH5Yi9!EfBS0iczeGLJ*TdA0#0Ra{JTNU&1r+8n}}4BF&G`1o$#-rzOy zkYJX%D36R>1uwsyaNXosaZb){fCe{j_L}qKVv2(Dy|C*8K5Mab5=gzU&^S1LMS5Rs zs($(`YH+2}TotP2E6tL=2CQ7yBNq#cAuvQa&(y)7l8!J7%MwJ;(I5bZ1xdA?`{3ql zhuIeB?`ddguG7)&#*^xCL4Br)|FTK|ARp(-VQ1G{;5XzWpVrjWEUm8k`T7Er)Y{%I zBqe1DH`KGG^b8MwZEXc3O;c&9u%Md)6dzzB^Ht8b&FSGA?laT@UhC|PP-iPBEd0`C zrc-hcD7yB@$SOnbcqJjg+~5i&S2X0Nj6hXZR|BhUDVUChg$2O&#S486jrinbQocsv zu9+EW2?-i{Lv!kt^>q=)9|tQdY!JfGxqL9x_wxD#lXCqwz-pYr>-hL*wv-%44B+<2 z@NjTu!ss8wHE#gSH;{&G# zj2=NW!5aQ&AQz-oZ!~ol&H(@7kgav4;Sntib}8lA2=chd$fV%uaFF{qf%^*{p*cSx zc33J!Y^UWt0|P(|kUacdJae2wGi#w*E3*v0kqax-eC-7ryhslwGE9TciC~+9C4c-E zf&lCnb1sDWr0=78AfNc_Mwsw2r>KO4$X$+vC@=vu7N(&@P8cVTCPnWVm+3W#|nCFs3VdKB1n#jk4u!g6xrP3iF;+?;Br7O=$pEg=F#DuOh% zIzk(S9wN&?!k^v;6QN5>OORBjAL~P2C434ajId#h?^lsx+Bsdq%91gA*tF^DN~afx zP0|g;3mbkm*)UShxP(F}D+e=(JSWj+3}y&@gPt1|T&93o+5HyX(yX|kI=~bwfZpug zM=5$e`|M(~F@vGN?2jDYopstq7?uLvG~Jx`&jtJjvZiJqWh0?k$I_~Y zS<+SCL7Mz-EOhEdRu4H~e!vvK2?fQU?Armpik)%|uHeGEa;6yb>Xm6mq35sfTOH9? z-o3MJ0m0VP%#0o$xt#Pn zkL}s>gLPftM7eo+H8r+=|2@4zCsArw4v!=*&ZGHfqIP&gf23GKgQ+t-4~?x^c>VYh zBNjzQMs^mMJ+<}aZy)659ZmU%4_TiatVxD%Pw2`*OXKvJw%KZIXlQ=dM_6P3^jzEz z9huv9k7$RUEv}21ns9+Hc7DV~{5l~vDe5Q%+;H8CyK|AZckfK^{IWWtb-~S%WeEPM z&u$joCWdr#mdB<0N@c;==2^&_ZOn?>l}ks-K^h~%8tvOkfMG0&Fcx3aG;}&Qd?j$1 zjp^O}^t5*wA3N+Q)75aGh8SsC-G`Dgm_5-x|7x)IRZNY!IYH~i0$+y3&CbI-yNrR= z_HQ%@34X-RYP-ur2Q|Z0!^)%72M&GEK`-tyg!3DfPQjaho{GpW?rtSs>>^pgrU0;M7C3;T5I(4gnLv86Ghmv+5A1mS}0jz2VR)gubuGTp_YR!!x_l*md99FP6 zii5mvn#n{|7WV<~`0|TT925sxw0O}qLTtzK@8jd(?wQnstx>5`NJ6iT#$Y-)J>B57 zwGY0@wEIcDxIRVKHh(r>{Ji-0dH)1P90M2QFQSdNE+^^!{dpfIS9>*Rw^N4O=z|Y< zwD=kihSx{lcH$Ee5rM0z!uXsc%EinHjl78!hDtOtz5Fyazb#WPoyNhey*9@vkfxD; z-Z%gAWgmmq7pSJRq&ZSPEc6hupZ@NrhjhGYzxD^N06>lM^gURs^x3t;Bcv* z0bA!SFqI?bK*gV*{{7h$;0MJI=6I&20zf*w_+GXRrZx{4F@o1CJvg}FI{zEnfvhX= z7o@>=4!1;Dv4s9^4*GW`(8ZI5Y!aA4@O>26{gA&R{X9Dx#%H(Taeyz+mZkyBeK7Cp zzh~W_7rFmTrIyd(+s=jHw9W5)iKk`P$j~z} zxil?BwC8*hb+)jWPhyn6yjG$4;=COsPF5Blcwo@CGytoJAW}C5pq$9STLTW54!SrG z!zO~(Cg4}uXJL)rzP?vrFoJ35dpfS5^TK(X@_-IO(*hkHWU^^BzD8InkoRRy4tla- zJKEcG@z{f(9^^4-cVSy>c-0eMKWYFjxHS|D=uki4HUa0rEPT**|t;0h_SvK-MJa8Gz}KU<{l?nZMIjuV06N1THKlX0d$*85G`1(Yl=_VD$w? z*s=mo)`3>j=J@FQCkXT&SAcXd%~4WP0)Fk2Zsx>N)5ag$e^33JfRMwT{}Rt{DE*_oc*o`Gbd>d_VC4X`&C|IWdXkqujaAn>pXnj4q7D={(HFk;Ke%)HMR^rXJJ zT7(3*Vef0N%u#Dgfg1fx(gMocXg5jBKsfzO)E~ z*k=QvAR&|?Cmq8TO!@`}t1E|JzkSQP9dPo3CkU3O{$yi8CB+F$8{+;)J6D3lsjr5U z)Xn^M5toAhhB49l{|wx};ZkA@gaPy6LuQc1u!*>MdFb56?imn)A6i;Euc`_RG;~!q zGa`?^CBh9*LmWlzABqG^`yEQ{v|xaR8*=>7GG9<5oG+o_9uM;i6BQQH zz6Wqjj)vMVK%zlVM*MzrZckRR|3*jIf*h>-b{5XW@Px?y{Mir4Fkzxuuc?nPbE7c= z(rj0Jy#SoJQ<&*gw^59CNOL*&1E~!Bq7Vz>?kk%RA~z&FZ2z5|q9M?QgF*(Hk!jV+ zaoPQ!kOQX=x9-Ee8T7FKeMbt#Kr9Eu`!VqR3-mf5L%eT1lt2lQQ5^TOS1f7W{zSh?7iE)9mWL z^q*1P<>kNAPgd!`h?qLH1rvgX7`lb(TQl{Ss2q%4+q`MuGU9+DGuIMA(iR?$iOYzx zJv+iHD3Aw70lWH3Z8Nj?cE(WafaM0RL{-%ulzN!Uf}tVMdZ9-;`Mce0KbDeD0Pui5 zsNpZ19Rc@Y{bf#++FPXmxerD6?BH$hoj7VBmKt5=wM>22wUA&qxko2YX|a_~bTb1w zOvs5KeVKBf8XS4O`4n8P4ZQCSuyd>pNCIn>qT6qx2+j(|;oqYQ9!mV4>p zv%sXsgrg!8sXvb3b>c8b>$Zc?90qm4Lskq+dy8UmGGG{yO+8lcNqUfmo z702elfXa7HpCwsY$5!{{ab4xZwV|@Ph=k|<-1DfPar%4;0x%?nOb9io&JfI}S^j$l zNN}c81|+=qr{_&L^d=r~QUq9XVJ{OSbGtrIiRwRrMTY&Yfr?7cO8>U7sHpUA$5r9U ze3l2;iCrT@^EikFD;No2o4GBIq_As&(yI>Q-V7h2-pLdWdkU_$qb80dMM2c$XYB}- zyq2B{j|_{8Q0ZNh3Mag3ji4rmhFq7rNi{}Hc*irHoeF0WL zR8YjZwkGpm!;H~Pa=@Vvk=*4RSmg^ND~-N?PPUxKHD9`1&FaU%A@&Dk`TRMdik221 z2M2(c6C$x`1c3*A=e=p86Quh2_B-(JwX0cvCkN{ohB)DhS(1O7*t2~6A;r|z)@J+f zxuQbBZ;XK?rKCVQ8Qa=w0zn48C!7`F^(F2af@qAuAuLj!8~qTI-++4esmLu{u?Bk^ zFwmZ!A_(a3jz62|uKuik{3tMxK3shO49BgVv1#0f6%0Z`RXI7y9Iw7#aUBG-d)bvm zL_qn&&2bS|Pb4QonMt*bk)r760=Z2eJz{TSod5ed5gtBR+^@2!E35tbkN>p>bqL;& zgB+%UF%cN)O1p#}M?3Q+`ehxgkyq$pAJ-#DGH&HHOoVAVcf({pCKMiWEe8*ew9k&Y zjg1Y!79*oc*t@QmgL3dReYUE{(UJ1T!@9k_)1E4NsK9|s$&uhogp>l@9h*i5CgSsF z%m9*tfZcFD1dfY%gUfsvc<}wRZy$%q?$rHKLXc-Ex>8w~h%5?)UVH83ix=1tU2N{| zekqtzoSdKrF^s%`-AB7?(aYAj6L+ysB? zfmNx-Rm`47U0r>1{Qb|qbazPFE-4^xL+L2+-fn>NZQ`i~a-Dyo5Ex9w`FFlu47fl; z{Q%DHxrFeh??0D1EWuv|g}>^M zM3`IF-rjrK2x$Sol3E6q+&21@j$_hZdUySuv&e5Y;%okhdPcAVHb1rWyy z^YsM|HV8uDeQMf=cdV6^dvtD{hN=u%;De+h)x3Kw0(;22qB>SkHV1; zn=FDU6{!q`thzc0OKPw~gKv=W1L*=N+aOO)I7DSZn(DEG#bN{7#?!7)>cqBX#i`?z z`tjhx48B)-PrE0J%NDj+k+#P+#q3cOytmE*FJ`<4G6bwvLa_*5eMaHJIbE&*D**IO zeQNwmf3aJ2->Ejj1Uv_)WA6j2y|#=w${&-HH{ID09rZtSo3EBRVQL&0e3v>#nbbxR z+0oGfltBbAh62^0B)ld^b9CO*(2{^9pw?g(wm%iO;hTo$P;??-h04WkGrqsolB1%j zGohI*U4>!=QVO3sFIL1h?WnHo|D%uIqP6&cSNo4!ioqJ~3(9MaZ~JbR++%AX0Bl)CzV>l^s4N%nVPHk&5HDXH-T4kO#N;wcNx!LP<8bMqkU{5 ze?8Pmy+VCoUW34A0Ao;SaF)c@Ss~fw`N`H;b6&nRt0<2u!O=H#M5^Z&u411|9=Ez0%Lb^Jl`h$U?Y%`^twHx@V~!MGYP9oxuJ2#IW)-V1uw0W{gCXHO>#$-k;H zJezzMZagZ1i@cT^p}@st=Xp(ri-sfzK1Iy4{ywY2yxSe0UCP*&7ff6E_4i@hM3Z=77pcmzTyGnyy0~ zD-iD5<)Pu^UX*ovIRdc^%TtwfvKckM%UJPk2t#Oex@|SHnL0G19}F)dtn#WaUE0}5 z+qrY>U4-tX5xG`#r1!fjVQb~ZcI#cq2{U{Y-}e-i+!OzqW!=HrUU%3nA?*fJF}qSR zTG2_L`FdWWVSC}NAt{ztSzKhehdX;dYMpK2-)GC>0j2f~4?$l;AzY?lhD!2`E9eM= z^zzVK{7^#O%?hGlVz0y&^d6sJ{%}i2{c;*^!&S{ciCqgE7L_lA^SiI{jvh#K<7$6_ zhhcQ>Pq6GA|JhaD4>~_cxI2Uy-gPmCBM502v;4QUs9V8=8mKSSq`tg&g%xIW7MbX{ zy_Aw=`>`GblOQy~cy#?^NH|LfiGciep%X^Ze6IAXR+180`vac})7C&Nn^&k`*S}kT zdyuLV#^P=%gp)zuWq^vQQ$11C4MVvXhxbrcbdvqJaN7e*=Upy;EGso|%q49E! ziM8`fE!XWwqxfUf*9hLDi+7mSg91Z7RC zNn`xy`FED(-?sat^;+y&zW2jP@(V(O)tRqPzgZ{o_**I@93@{R6D4Z;>Z+hs@%SAN ztSXWXR~)9V`gUD5M{Zvf88I2GZ8s=vb>3m45dPkE?qIIBc|;nd-jr@b8QjYsG0&3N z7o8$JU(Cd#@8;M!J#Hy1!xYf}?9_9O_qpziKItZ&{6|dr7vlE3QLGAb3A6zbe{XA3 z${FQZuwPM2u4G|T>XB%pV1B@2!nIxdJTON4`~-}z$g(~kE$`UaY z+KYRggeI#F2M(ItH|!YY;D4vuZhA1=LZHgWvQ@*wT6EvyCN7l%V~4971L3MWe3*!Se(?YJ;=6lQbS{J(>SZ zomlSCvA*Tk0|$x>*8l-ym3FEbPJ#J~n%C@Fg)hH^>P_RzDwo$<{ur2O(z|xQNmd~4 zq`2@6aZ?BnTCX}eV%02yJw(bH=YT$u9JBJfK!04M8eb(OW#c2BqGOLF=U2v|p!kO` z!t?l(Eq@n#EL;nnyH6)bsdH@ju$Pjkv%G2#4M`u3F|9x=IWXbHv8uDop-8$Nz3FSw zuOD^YckqrV0W;eRfyW%NOsa94Cb+5d9fS`tE$n%J=e|p%IR!$m`r6KpxrQ;`FYVwCS zj2-{V!}?V_M}L&(oW=bZ!$cUUd1e}d-NJ1->(sH_;^#KgooNKX*@7sGsWDF=m9!^5yD`vHd_w=6wsS839|%iwx9 zGp#BPk!^_eqqfi==)^1^r#0rsJJ5NILS9imH_CXTE_g#l9vAVZx<{pXySn?0vQng% zzgK#2;CqdXir#LW)n~V2Czu~AUwu*{8b{nYI`V-iQ*Yy9r&sl6;jo+BkRFGtgvC}a z`TVABQwV{Yi8Uq1m4*fU*5QL9tEWxEQZ&3ePK~9)33Ya)E-xFpl+lo+C*-^VsT$Pv z1;wLFYt^4vtO{Go5QaB@Y^^p0g7kV{=9zHWNz{t)|#8}Gv;`1RQTn45@~9PfR2-Y3Vm=4fUlaXmGZBPE$C z+9+2z)=i4#&{90(lckU<`JEDfJKge6r8zUcDTf(ed-4)C42Pj|q3($0_?0ge*Pgsd ztr`B4(aFi2+NqGl7XLlga6hj4Uf0ZX7k6#4>U5I_QCT%{0blRX=#=39W^)lv?~^{6 zpa_Y3V3}k0(WA|NcQLNSUEI}*n080SVAPK9uFd()3+-EJL${)&hj|zh-hPzVZ*TWw zn-bw_Ow(8%TdDGAh_Q?AsfX>_O(XI%_5)1?EnmI0E(fbtlRM#rv3kto*B{hI++*tw zjh@ZjV$J#H8(d>;^dW*;Lssc}4<(6Kg4^lPyIoK2JhU{tXd%vx0OI*uev65J!pMpB z<+Ph=Ey{;5oBU}Ri}E()hDi^l5ol4(ZWS2FvVi#0F4 zl&DAH+kJ1-zq_I{Q!!_uuxd!Eb_OK~;z z?tPytcgHQc*K~)>k92KQj)y)e6h|FuC4VA*{-I5EuNr(Hh1jqLv?eis^>s64sd_buZn=qOXidGRkQ|5`C&d0*oT6#XL%mLx zRI_Xk=QTOHDBe5Phk1n8I6v7XJv~V>!KxvidMKf@&|J>2NK@Uxz)F?zxPv>afG@ab z0Z&qk$C2FCOk0rWSsu2n#*T`ZAm50SnR5#l;XGAP>5bh_lAnHKulA6lrWZYIY%qgs zF;G{yk9$?sx`hTF{Ze5~IjY#kOlr+F8q}4i^uy8&eZNd5_Uq1AP4%^LM(*H;b%|>~ zLPeqmR(8X}gPtWhcST1joID?@crgD=^>#LuC6yp&x>*swIxRuBzzQ9+%+hlsEA(NL zsC8!Rel&*qEs}-!ZihTNPN5GgajA_-j{5PRuvpSHhT}4Q8IAgKDo)ehQsn;m`u)cX z&S=iKhv@cvw}h)m?Td|%6_wmR^^&{(^Tf723vmc|a)-Kb`d5N4&l6Ln8fE4R#3I?k z(I=^&y=MA4$!bq>*qL!w(BYkJ0&5TXvBjUu%g8m}?tC#@kJ-%@bCvWW)&7Sa-$n1q513f(;%KP&H0rP^c@W;x;O{r|W_lv>x<7yC zMr~)GNLk-?s$MGeaM7o-{t}BvI2NxO@;do#xb7F;Y~y^!E-grm?s#0=9y^@h_Qbt} zSiY{W@K#rr7^{Mfr*Idq>9}~-_fw- zrQG7ox=rr!Rb=#`m90)ymW_28r-Z2SCK8qM;#4y%K4yvJGnWw!>f;L?_nnV7nSDmn z{EXa1MIO(zQ_BYY!BJty`U}^-6k7(g$QayIQDh8vc!kkXdMt`MWHw^!VfdY@dWgn( zLG$N{y_X52d3}pCHdn8g3VG#J5!ERho5Egn&ra1#bDh2jdXCJEO6n~CKUQXR67M)7 z#N$5QzV+2dZIe!=e>8Y5y=tu^ztSw9u_gmMs)fuRx;;?^c&6#Hy;>6`_@gDPqL3r;j$gW>_q=T>nxd?foV49TGz zA9sl!p6kaqrG>^xQ=Bg*72=bi^cG@}z^Q9AmTTD$ti7rZwgsP8;{lG?P|3u8tg463c#>Fvsd% z7`jfIcV*B)QAC91E}Jcl7#vFcZ}Ohdddx}yN%Qk((@`QfXoTW}*%atdZbQ#sj!6eE zNwWAp{G0Aef0I$MH=pgqduWEar*iT~Q|;#HM~+?WXJstYGe{jy(njMBtEn#&L;I0y zH@jG5=V`|1`;l&zoVPjE6K4+3YJ9#>7;|AJX3jT9moXr+9oN0Bp3eJu^0y7%X{b1O zAkIRWPPg3D&@J=4{~ucjV=?8lN13%Z2Wt#94+=Q&e)t)oq&VI3?U89Z8_$OyKQL79!%X;PN{$9lTy*S}8?~a9H7;|(v_i6YwYtR0+RG==w++%IE47niyrkkj+Te7XuLm)Vu!hhi zzYt=$PEAzwj(0Fp!A**waMqWZgrUCD`O2$SMrucquhk!HP|~+r1NSoQzP&dN_A%Oy zJttFayMgvB9FL7rPwYCnWd?N{a4wp~(|+3~?u*4I_CU;5&kM)sdm6NE2`-Dfa8pe|cuJJ7;MS)ItQOmi&#xk7@< zz*afwEM18D^K5nKT`S9p>-gw#2;nA*Q6g8mm%cihS@XdhIuga7>3V>#*_L`l+)YR% zluOx$hAYR?)_8}{sBKUqPT5K=oixEseAp+Jj&dN<`~Fjl;pWd+$ghLluxIMOnJTOT zU#jRw?$@%IS3Ki)LhI>Rw!$uXWUl&HG`?GpE9TB)ogZ?>liEs@fALU@WOOR=lQ$E{ z$JvubEEFC;-OZN$C}#JSFS^A)x+U{xS<*16eTQMIMTs+-fK8M^F7hB}PSYiJkuY3k zBdT$Y@0DgsJFI&BrJOrU4tR=O6+btvCwi`5^HzP1 zJdDt%aIi(yQLIP3Q7Ljbi|Q_(Fc=i+@scaO8j*Z8;D>~v+5vei-s%@XaTIn1(GWoUm`@vd)SgeCEw&)>V>n}xT$(HDb|#t9SKJjFPAgd-r7%MA?-N3_W`K#`eBd ziR99(Dqd&CsniuzLhKRVDB4jpn<=I8>El0HQ z!aKRq?zJ*o>YNgV$XykM-+Ce&Z*h-3vi4V!UYKa7r6;-WJ}Z>>HX}po95PJGw;EKa zs9lfApL*DQ*?7(;%kQ+`L5j6v;Bmc{pUvKw1xrB7W~g0NeWh96Gdwt$C_`SAUK9{n2_BR` zS$%0ZmtfM3-%{!G3QKZRZt_{XZ+-pQ}Z2j4A}1O)*~METOxii}~Xi-Cj4UNfi~L!P|H5jTW9p z@vTX2ONG>OiZ``d5mB*kISGCtrl>k9++7rz3dutKK$&RGigg)Ael`ruU7*~`)f%hl zuhvo8)T~s_4GV4<^Y(NG$L8+w@`yAQ#M7)+n1y?$mj<+}J=)VG+gH2hAa2nyakYAN zh3)D5rqI%<NeY%zzQ~L3UD_4qUTWlTU+ZDG{9!vRRJWF>k zH@uksPHH56bys0ri+!%N%H~r*_sZ?GA0yVT#+pMR`QA%1b-f;#1>tvuTQ9Cyqs|Hz THE)=LJswd})Kn-(nTP%lH}M8p literal 0 HcmV?d00001 diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9fa9578a8..8f83b9449 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -77,6 +77,9 @@ dependencies: auth0-js: specifier: ^9.22.1 version: 9.22.1 + aws-sdk: + specifier: ^2.1492.0 + version: 2.1499.0 bcrypt: specifier: ^5.1.0 version: 5.1.0 @@ -218,6 +221,9 @@ dependencies: uuid: specifier: ^9.0.0 version: 9.0.0 + validator: + specifier: ^13.11.0 + version: 13.11.0 web-push: specifier: ^3.6.4 version: 3.6.4 @@ -2670,6 +2676,22 @@ packages: resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==} engines: {node: '>= 0.4'} + /aws-sdk@2.1499.0: + resolution: {integrity: sha512-kh89lcXx7lP83uVjzRPkOueRoM8gQlep86W9+l3qCTHSLiVJuc0MiPmqCLMPlOAZil+35roFkwWIP2FJ1WcdXg==} + engines: {node: '>= 10.0.0'} + dependencies: + buffer: 4.9.2 + events: 1.1.1 + ieee754: 1.1.13 + jmespath: 0.16.0 + querystring: 0.2.0 + sax: 1.2.1 + url: 0.10.3 + util: 0.12.5 + uuid: 8.0.0 + xml2js: 0.5.0 + dev: false + /aws-sign2@0.7.0: resolution: {integrity: sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==} requiresBuild: true @@ -2968,6 +2990,14 @@ packages: engines: {node: '>=4'} dev: false + /buffer@4.9.2: + resolution: {integrity: sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==} + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + isarray: 1.0.0 + dev: false + /buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} dependencies: @@ -3154,7 +3184,7 @@ packages: dependencies: '@types/validator': 13.9.0 libphonenumber-js: 1.10.39 - validator: 13.9.0 + validator: 13.11.0 /clean-stack@2.2.0: resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} @@ -4272,6 +4302,11 @@ packages: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} + /events@1.1.1: + resolution: {integrity: sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw==} + engines: {node: '>=0.4.x'} + dev: false + /events@3.3.0: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} @@ -5092,6 +5127,10 @@ packages: url-join: 4.0.1 dev: false + /ieee754@1.1.13: + resolution: {integrity: sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==} + dev: false + /ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} @@ -5295,6 +5334,13 @@ packages: engines: {node: '>=6'} dev: true + /is-generator-function@1.0.10: + resolution: {integrity: sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: false + /is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} @@ -5895,6 +5941,11 @@ packages: - ts-node dev: true + /jmespath@0.16.0: + resolution: {integrity: sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==} + engines: {node: '>= 0.6.0'} + dev: false + /jose@4.14.4: resolution: {integrity: sha512-j8GhLiKmUAh+dsFXlX1aJCbt5KMibuKb+d7j1JaOJG6s2UjX1PQlW+OKB/sD4a/5ZYF4RcmYmLSndOoU3Lt/3g==} dev: false @@ -7294,6 +7345,10 @@ packages: end-of-stream: 1.4.4 once: 1.4.0 + /punycode@1.3.2: + resolution: {integrity: sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==} + dev: false + /punycode@2.3.0: resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==} engines: {node: '>=6'} @@ -7365,6 +7420,12 @@ packages: dev: false optional: true + /querystring@0.2.0: + resolution: {integrity: sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==} + engines: {node: '>=0.4.x'} + deprecated: The querystring API is considered Legacy. new code should use the URLSearchParams API instead. + dev: false + /queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} dev: true @@ -7672,6 +7733,10 @@ packages: /safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + /sax@1.2.1: + resolution: {integrity: sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==} + dev: false + /sax@1.2.4: resolution: {integrity: sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==} dev: false @@ -8770,6 +8835,13 @@ packages: resolution: {integrity: sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==} dev: false + /url@0.10.3: + resolution: {integrity: sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ==} + dependencies: + punycode: 1.3.2 + querystring: 0.2.0 + dev: false + /urlpattern-polyfill@9.0.0: resolution: {integrity: sha512-WHN8KDQblxd32odxeIgo83rdVDE2bvdkb86it7bMhYZwWKJz0+O0RK/eZiHYnM+zgt/U7hAHOlCQGfjjvSkw2g==} dev: false @@ -8795,6 +8867,16 @@ packages: inherits: 2.0.3 dev: false + /util@0.12.5: + resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==} + dependencies: + inherits: 2.0.4 + is-arguments: 1.1.1 + is-generator-function: 1.0.10 + is-typed-array: 1.1.12 + which-typed-array: 1.1.11 + dev: false + /utils-merge@1.0.1: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} @@ -8807,6 +8889,11 @@ packages: dev: false optional: true + /uuid@8.0.0: + resolution: {integrity: sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==} + hasBin: true + dev: false + /uuid@8.3.2: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} hasBin: true @@ -8829,8 +8916,8 @@ packages: convert-source-map: 1.9.0 dev: true - /validator@13.9.0: - resolution: {integrity: sha512-B+dGG8U3fdtM0/aNK4/X8CXq/EcxU2WPrPEkJGslb47qyHsxmbggTWK0yEA4qnYVNF+nxNlN88o14hIcPmSIEA==} + /validator@13.11.0: + resolution: {integrity: sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==} engines: {node: '>= 0.10'} /vary@1.1.2: @@ -9128,6 +9215,14 @@ packages: xmlbuilder: 11.0.1 dev: false + /xml2js@0.5.0: + resolution: {integrity: sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==} + engines: {node: '>=4.0.0'} + dependencies: + sax: 1.2.4 + xmlbuilder: 11.0.1 + dev: false + /xmlbuilder@11.0.1: resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==} engines: {node: '>=4.0'} From 78583ca2788cea94f8a5cb193ef812332cb094c2 Mon Sep 17 00:00:00 2001 From: bhavanakarwade Date: Mon, 20 Nov 2023 20:27:59 +0530 Subject: [PATCH 41/62] added keys Signed-off-by: bhavanakarwade --- libs/aws/src/aws.service.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libs/aws/src/aws.service.ts b/libs/aws/src/aws.service.ts index 443ab7328..d11acc734 100644 --- a/libs/aws/src/aws.service.ts +++ b/libs/aws/src/aws.service.ts @@ -9,9 +9,9 @@ export class AwsService { constructor() { this.s3 = new S3({ - accessKeyId: process.env.AWS_PUBLIC_ACCESS_KEY, - secretAccessKey: process.env.AWS_PUBLIC_SECRET_KEY, - region: process.env.AWS_PUBLIC_REGION + accessKeyId: process.env.AWS_ACCESS_KEY, + secretAccessKey: process.env.AWS_SECRET_KEY, + region: process.env.AWS_REGION }); } From ecacc612eac4622e753eb3f1d03e95583583470b Mon Sep 17 00:00:00 2001 From: bhavanakarwade Date: Mon, 20 Nov 2023 20:30:15 +0530 Subject: [PATCH 42/62] remove unnecessary files Signed-off-by: bhavanakarwade --- apps/user/templates/background_image.png | Bin 68796 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 apps/user/templates/background_image.png diff --git a/apps/user/templates/background_image.png b/apps/user/templates/background_image.png deleted file mode 100644 index 15b7f3be6777ec547e8c872a54aacc7fbb903b03..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 68796 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 rxz9;wHXcC)O3y} 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�^=H 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 THE)=LJswd})Kn-(nTP%lH}M8p From 3926702c584be82c0b5443e1f9fc9c08237f9e3d Mon Sep 17 00:00:00 2001 From: bhavanakarwade Date: Mon, 20 Nov 2023 20:33:01 +0530 Subject: [PATCH 43/62] uploaded files Signed-off-by: bhavanakarwade --- libs/aws/src/aws.service.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/libs/aws/src/aws.service.ts b/libs/aws/src/aws.service.ts index d11acc734..8e1dcbd72 100644 --- a/libs/aws/src/aws.service.ts +++ b/libs/aws/src/aws.service.ts @@ -6,6 +6,7 @@ import { promisify } from 'util'; @Injectable() export class AwsService { private s3: S3; + private s4: S3; constructor() { this.s3 = new S3({ @@ -13,6 +14,11 @@ export class AwsService { secretAccessKey: process.env.AWS_SECRET_KEY, region: process.env.AWS_REGION }); + this.s4 = new S3({ + accessKeyId: process.env.AWS_PUBLIC_ACCESS_KEY, + secretAccessKey: process.env.AWS_PUBLIC_SECRET_KEY, + region: process.env.AWS_PUBLIC_REGION + }); } async uploadUserCertificate( @@ -24,7 +30,7 @@ export class AwsService { filename: string = 'cerficate' ): Promise { const timestamp = Date.now(); - const putObjectAsync = promisify(this.s3.putObject).bind(this.s3); + const putObjectAsync = promisify(this.s4.putObject).bind(this.s4); try { await putObjectAsync({ From cfea966a54a8cf2d9fa8457a0265c8e5e510d523 Mon Sep 17 00:00:00 2001 From: bhavanakarwade Date: Mon, 20 Nov 2023 21:07:38 +0530 Subject: [PATCH 44/62] feat: added headless in pupeeter launch Signed-off-by: bhavanakarwade --- apps/user/src/user.service.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/user/src/user.service.ts b/apps/user/src/user.service.ts index e6e0b7006..5501e76fd 100644 --- a/apps/user/src/user.service.ts +++ b/apps/user/src/user.service.ts @@ -613,7 +613,9 @@ export class UserService { } async convertHtmlToImage(template: string, credentialId: string): Promise { - const browser = await puppeteer.launch(); + const browser = await puppeteer.launch({ + headless:'new' + }); const page = await browser.newPage(); await page.setContent(template); From e5f732a3f35606fe1d98dc1ee260344e34e33689 Mon Sep 17 00:00:00 2001 From: tipusinghaw <126460794+tipusinghaw@users.noreply.github.com> Date: Tue, 21 Nov 2023 00:14:00 +0530 Subject: [PATCH 45/62] feat: added logger for socket (#272) Signed-off-by: tipusinghaw --- apps/issuance/src/issuance.service.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/issuance/src/issuance.service.ts b/apps/issuance/src/issuance.service.ts index b6897810c..1bd4a883f 100644 --- a/apps/issuance/src/issuance.service.ts +++ b/apps/issuance/src/issuance.service.ts @@ -890,6 +890,8 @@ export class IssuanceService { }); } + this.logger.log(`jobDetails.clientId----${JSON.stringify(jobDetails.clientId)}`); + const socket = await io(`${process.env.SOCKET_HOST}`, { reconnection: true, reconnectionDelay: 5000, From f60515ef11e8065ead8660d146afa6830b4e8b80 Mon Sep 17 00:00:00 2001 From: tipusinghaw <126460794+tipusinghaw@users.noreply.github.com> Date: Tue, 21 Nov 2023 08:53:25 +0530 Subject: [PATCH 46/62] fix: changed socket emit logic (#273) * feat: added logger for socket Signed-off-by: tipusinghaw * fix: changed socket logic Signed-off-by: tipusinghaw --------- Signed-off-by: tipusinghaw --- apps/issuance/src/issuance.service.ts | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/apps/issuance/src/issuance.service.ts b/apps/issuance/src/issuance.service.ts index 1bd4a883f..ec15141f0 100644 --- a/apps/issuance/src/issuance.service.ts +++ b/apps/issuance/src/issuance.service.ts @@ -43,7 +43,7 @@ export class IssuanceService { @InjectQueue('bulk-issuance') private bulkIssuanceQueue: Queue ) { } - + async sendCredentialCreateOffer(orgId: number, user: IUserRequest, credentialDefinitionId: string, comment: string, connectionId: string, attributes: object[]): Promise { try { const agentDetails = await this.issuanceRepository.getAgentEndPoint(orgId); @@ -821,9 +821,16 @@ export class IssuanceService { } } - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types - async processIssuanceData(jobDetails): Promise { - + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/explicit-function-return-type + async processIssuanceData(jobDetails) { + const socket = await io(`${process.env.SOCKET_HOST}`, { + reconnection: true, + reconnectionDelay: 5000, + reconnectionAttempts: Infinity, + autoConnect: true, + transports: ['websocket'] + }); + const fileUploadData: FileUploadData = { fileUpload: '', fileRow: '', @@ -864,7 +871,6 @@ export class IssuanceService { if (oobCredentials) { await this.issuanceRepository.deleteFileDataByJobId(jobDetails.id); } - return oobCredentials; } catch (error) { this.logger.error( `error in issuanceBulkCredential for data ${jobDetails} : ${error}` @@ -892,13 +898,6 @@ export class IssuanceService { this.logger.log(`jobDetails.clientId----${JSON.stringify(jobDetails.clientId)}`); - const socket = await io(`${process.env.SOCKET_HOST}`, { - reconnection: true, - reconnectionDelay: 5000, - reconnectionAttempts: Infinity, - autoConnect: true, - transports: ['websocket'] - }); socket.emit('bulk-issuance-process-completed', { clientId: jobDetails.clientId }); } } catch (error) { From 218d82f282ad1a2f1cfa13fa58b85158855d8dcf Mon Sep 17 00:00:00 2001 From: KulkarniShashank Date: Tue, 21 Nov 2023 13:52:05 +0530 Subject: [PATCH 47/62] fix: added label for the out-of-band issuance Signed-off-by: KulkarniShashank --- apps/agent-service/src/agent-service.service.ts | 2 +- apps/issuance/src/issuance.service.ts | 3 ++- apps/verification/src/interfaces/verification.interface.ts | 3 ++- apps/verification/src/verification.service.ts | 1 + 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/apps/agent-service/src/agent-service.service.ts b/apps/agent-service/src/agent-service.service.ts index dc6adb010..02cba6d30 100644 --- a/apps/agent-service/src/agent-service.service.ts +++ b/apps/agent-service/src/agent-service.service.ts @@ -305,7 +305,7 @@ export class AgentServiceService { if (agentSpinupDto.clientSocketId) { socket.emit('invitation-url-creation-started', { clientId: agentSpinupDto.clientSocketId }); } - await this._createLegacyConnectionInvitation(orgData.id, user, agentPayload.walletName); + await this._createLegacyConnectionInvitation(orgData.id, user, walletProvisionPayload.orgName); if (agentSpinupDto.clientSocketId) { socket.emit('invitation-url-creation-success', { clientId: agentSpinupDto.clientSocketId }); } diff --git a/apps/issuance/src/issuance.service.ts b/apps/issuance/src/issuance.service.ts index ec15141f0..c9d33d148 100644 --- a/apps/issuance/src/issuance.service.ts +++ b/apps/issuance/src/issuance.service.ts @@ -293,7 +293,8 @@ export class IssuanceService { } }, autoAcceptCredential: 'always', - comment + comment, + label: organizationDetails?.name }; diff --git a/apps/verification/src/interfaces/verification.interface.ts b/apps/verification/src/interfaces/verification.interface.ts index 99ad821f7..a61876a8f 100644 --- a/apps/verification/src/interfaces/verification.interface.ts +++ b/apps/verification/src/interfaces/verification.interface.ts @@ -84,12 +84,13 @@ export interface ISendProofRequestPayload { connectionId?: string; proofFormats: IProofFormats; autoAcceptProof: string; + label: string; } export interface IProofRequestPayload { url: string; apiKey: string; - proofRequestPayload: ISendProofRequestPayload + proofRequestPayload: ISendProofRequestPayload; } interface IWebhookPresentationProof { diff --git a/apps/verification/src/verification.service.ts b/apps/verification/src/verification.service.ts index 7d49ff2b2..e432d2dd4 100644 --- a/apps/verification/src/verification.service.ts +++ b/apps/verification/src/verification.service.ts @@ -344,6 +344,7 @@ export class VerificationService { proofRequestPayload: { protocolVersion, comment, + label: organizationDetails?.name, proofFormats: { indy: { name: 'Proof Request', From dc9590c3d00054321f94c97fc710c7d7a615a1d6 Mon Sep 17 00:00:00 2001 From: KulkarniShashank Date: Tue, 21 Nov 2023 14:46:21 +0530 Subject: [PATCH 48/62] Response changes on the schema and credential definition list Signed-off-by: KulkarniShashank --- .../credential-definition.repository.ts | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/apps/ledger/src/credential-definition/repositories/credential-definition.repository.ts b/apps/ledger/src/credential-definition/repositories/credential-definition.repository.ts index 7710d9f95..0db669d4f 100644 --- a/apps/ledger/src/credential-definition/repositories/credential-definition.repository.ts +++ b/apps/ledger/src/credential-definition/repositories/credential-definition.repository.ts @@ -168,7 +168,7 @@ export class CredentialDefinitionRepository { } } - + async getAllCredDefsByOrgIdForBulk(payload: BulkCredDefSchema): Promise { try { const { credDefSortBy, sortValue, orgId } = payload; @@ -190,7 +190,7 @@ export class CredentialDefinitionRepository { }); const schemaLedgerIdArray = credentialDefinitions.map((credDef) => credDef.schemaLedgerId); - + const schemas = await this.prisma.schema.findMany({ where: { schemaLedgerId: { @@ -201,10 +201,11 @@ export class CredentialDefinitionRepository { name: true, version: true, schemaLedgerId: true, - orgId: true + orgId: true, + attributes: true } }); - + // Match Credential Definitions with Schemas and map to CredDefSchema const matchingSchemas = credentialDefinitions.map((credDef) => { @@ -213,12 +214,16 @@ export class CredentialDefinitionRepository { if (matchingSchema) { return { credentialDefinitionId: credDef.credentialDefinitionId, - schemaCredDefName: `${matchingSchema.name}:${matchingSchema.version}-${credDef.tag}` + schemaCredDefName: `${matchingSchema.name}:${matchingSchema.version}-${credDef.tag}`, + schemaName: matchingSchema.name, + schemaVersion: matchingSchema.version, + schemaAttributes: matchingSchema.attributes, + credentialDefinition: credDef.tag }; } return null; }); - + // Filter out null values (missing schemas) and return the result return matchingSchemas.filter((schema) => null !== schema) as CredDefSchema[]; } catch (error) { @@ -226,6 +231,6 @@ export class CredentialDefinitionRepository { throw error; } } - - + + } \ No newline at end of file From c458d1516167fba63f62506374d18d6272cade3c Mon Sep 17 00:00:00 2001 From: KulkarniShashank Date: Tue, 21 Nov 2023 15:45:36 +0530 Subject: [PATCH 49/62] fix: solved the bug for out-of-band verification Signed-off-by: KulkarniShashank --- apps/verification/src/interfaces/verification.interface.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/verification/src/interfaces/verification.interface.ts b/apps/verification/src/interfaces/verification.interface.ts index a61876a8f..26f725065 100644 --- a/apps/verification/src/interfaces/verification.interface.ts +++ b/apps/verification/src/interfaces/verification.interface.ts @@ -84,7 +84,7 @@ export interface ISendProofRequestPayload { connectionId?: string; proofFormats: IProofFormats; autoAcceptProof: string; - label: string; + label?: string; } export interface IProofRequestPayload { From d4cd036351269a675055e470d79b7cccb95298f8 Mon Sep 17 00:00:00 2001 From: KulkarniShashank Date: Tue, 21 Nov 2023 16:11:24 +0530 Subject: [PATCH 50/62] fix: solved the bug for connection label Signed-off-by: KulkarniShashank --- apps/agent-service/src/agent-service.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/agent-service/src/agent-service.service.ts b/apps/agent-service/src/agent-service.service.ts index 02cba6d30..9909765cf 100644 --- a/apps/agent-service/src/agent-service.service.ts +++ b/apps/agent-service/src/agent-service.service.ts @@ -305,7 +305,7 @@ export class AgentServiceService { if (agentSpinupDto.clientSocketId) { socket.emit('invitation-url-creation-started', { clientId: agentSpinupDto.clientSocketId }); } - await this._createLegacyConnectionInvitation(orgData.id, user, walletProvisionPayload.orgName); + await this._createLegacyConnectionInvitation(orgData.id, user, orgData.name); if (agentSpinupDto.clientSocketId) { socket.emit('invitation-url-creation-success', { clientId: agentSpinupDto.clientSocketId }); } From 3ada156cd1c3e9b9a7ceaf35c927c210b2ba579c Mon Sep 17 00:00:00 2001 From: KulkarniShashank Date: Tue, 21 Nov 2023 16:14:21 +0530 Subject: [PATCH 51/62] fix: solved the bug for connection label Signed-off-by: KulkarniShashank --- apps/agent-service/src/agent-service.service.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/agent-service/src/agent-service.service.ts b/apps/agent-service/src/agent-service.service.ts index 9909765cf..89d717489 100644 --- a/apps/agent-service/src/agent-service.service.ts +++ b/apps/agent-service/src/agent-service.service.ts @@ -305,6 +305,8 @@ export class AgentServiceService { if (agentSpinupDto.clientSocketId) { socket.emit('invitation-url-creation-started', { clientId: agentSpinupDto.clientSocketId }); } + + this.logger.log(`orgData.name ::: ${orgData.name}`); await this._createLegacyConnectionInvitation(orgData.id, user, orgData.name); if (agentSpinupDto.clientSocketId) { socket.emit('invitation-url-creation-success', { clientId: agentSpinupDto.clientSocketId }); From a6508b0d3cdbc940d7e57bde3e27e0093b2d6be6 Mon Sep 17 00:00:00 2001 From: bhavanakarwade Date: Tue, 21 Nov 2023 16:22:34 +0530 Subject: [PATCH 52/62] refactor: resolved puppeteer library errors Signed-off-by: bhavanakarwade --- Dockerfiles/Dockerfile.user | 32 ++- .../organization/organization.controller.ts | 27 --- .../src/organization/organization.service.ts | 10 - apps/user/src/user.service.ts | 14 +- apps/user/templates/winner-template.ts | 123 +++++------- package.json | 2 + pnpm-lock.yaml | 182 +++++++++++++++++- 7 files changed, 266 insertions(+), 124 deletions(-) diff --git a/Dockerfiles/Dockerfile.user b/Dockerfiles/Dockerfile.user index 5f6f4bd73..4b59f0a25 100644 --- a/Dockerfiles/Dockerfile.user +++ b/Dockerfiles/Dockerfile.user @@ -1,6 +1,20 @@ # Stage 1: Build the application -FROM node:18-alpine as build +FROM node:18-slim as build RUN npm install -g pnpm + +# We don't need the standalone Chromium +ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD true + +# Install Google Chrome Stable and fonts +# Note: this installs the necessary libs to make the browser work with Puppeteer. +RUN apt-get update && apt-get install gnupg wget -y && \ + wget --quiet --output-document=- https://dl-ssl.google.com/linux/linux_signing_key.pub | gpg --dearmor > /etc/apt/trusted.gpg.d/google-archive.gpg && \ + sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' && \ + apt-get update && \ + apt-get install google-chrome-stable -y --no-install-recommends && \ + rm -rf /var/lib/apt/lists/* + +# RUN apk update && apk list --all-versions chromium # Set the working directory WORKDIR /app @@ -18,7 +32,19 @@ RUN cd libs/prisma-service && npx prisma migrate deploy && npx prisma generate RUN pnpm run build user # Stage 2: Create the final image -FROM node:18-alpine +FROM node:18-slim + +# We don't need the standalone Chromium +ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD true + +# Install Google Chrome Stable and fonts +# Note: this installs the necessary libs to make the browser work with Puppeteer. +RUN apt-get update && apt-get install gnupg wget -y && \ + wget --quiet --output-document=- https://dl-ssl.google.com/linux/linux_signing_key.pub | gpg --dearmor > /etc/apt/trusted.gpg.d/google-archive.gpg && \ + sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' && \ + apt-get update && \ + apt-get install google-chrome-stable -y --no-install-recommends && \ + rm -rf /var/lib/apt/lists/* # Set the working directory WORKDIR /app @@ -38,4 +64,4 @@ CMD ["sh", "-c", "cd libs/prisma-service && npx prisma migrate deploy && npx pri # docker build -t user -f Dockerfiles/Dockerfile.user . # docker run -d --env-file .env --name user docker.io/library/user -# docker logs -f user +# docker logs -f user \ No newline at end of file diff --git a/apps/api-gateway/src/organization/organization.controller.ts b/apps/api-gateway/src/organization/organization.controller.ts index f2b17e0a2..09a9f1571 100644 --- a/apps/api-gateway/src/organization/organization.controller.ts +++ b/apps/api-gateway/src/organization/organization.controller.ts @@ -51,33 +51,6 @@ export class OrganizationController { return res.send(getImageBuffer); } - @Get('/certificate') - async convertHtmlToImage(@Res() res: Response): Promise { - - const htmlString = ` - - - - -
-
- Header -
-
- footer -
-
- -`; - - const imageBuffer = await this.organizationService.convertHtmlToImage(htmlString); - - res.set('Content-Type', 'image/png'); - return res.status(HttpStatus.OK).send(imageBuffer); - - } - - /** * * @param user diff --git a/apps/api-gateway/src/organization/organization.service.ts b/apps/api-gateway/src/organization/organization.service.ts index 34b1cadd8..52801680e 100644 --- a/apps/api-gateway/src/organization/organization.service.ts +++ b/apps/api-gateway/src/organization/organization.service.ts @@ -9,7 +9,6 @@ import { BulkSendInvitationDto } from './dtos/send-invitation.dto'; import { UpdateUserRolesDto } from './dtos/update-user-roles.dto'; import { UpdateOrganizationDto } from './dtos/update-organization-dto'; import { GetAllUsersDto } from '../user/dto/get-all-users.dto'; -import * as puppeteer from 'puppeteer'; @Injectable() export class OrganizationService extends BaseService { @@ -144,14 +143,5 @@ export class OrganizationService extends BaseService { return this.sendNats(this.serviceProxy, 'fetch-organization-profile', payload); } - async convertHtmlToImage(html: string): Promise { - const browser = await puppeteer.launch(); - const page = await browser.newPage(); - await page.setContent(html); - const screenshot = await page.screenshot({path: 'cert1.png'}); - - await browser.close(); - return screenshot; - } } diff --git a/apps/user/src/user.service.ts b/apps/user/src/user.service.ts index 5501e76fd..dd4dd3bdc 100644 --- a/apps/user/src/user.service.ts +++ b/apps/user/src/user.service.ts @@ -46,11 +46,11 @@ import { EcosystemConfigSettings, UserCertificateId } from '@credebl/enum/enum'; import { WinnerTemplate } from '../templates/winner-template'; import { ParticipantTemplate } from '../templates/participant-template'; import { ArbiterTemplate } from '../templates/arbiter-template'; -import * as puppeteer from 'puppeteer'; import validator from 'validator'; import { DISALLOWED_EMAIL_DOMAIN } from '@credebl/common/common.constant'; import { AwsService } from '@credebl/aws'; -import { readFileSync } from 'fs'; +import puppeteer from 'puppeteer'; + @Injectable() export class UserService { constructor( @@ -582,7 +582,8 @@ export class UserService { throw new NotFoundException('error in get attributes'); } - const imageBuffer = await this.convertHtmlToImage(template, shareUserCertificate.credentialId); + const imageBuffer = + await this.convertHtmlToImage(template, shareUserCertificate.credentialId); const verifyCode = uuidv4(); const imageUrl = await this.awsService.uploadUserCertificate( @@ -592,7 +593,6 @@ export class UserService { 'certificates', 'base64' ); - const existCredentialId = await this.userRepository.getUserCredentialsById(shareUserCertificate.credentialId); if (existCredentialId) { @@ -614,10 +614,12 @@ export class UserService { async convertHtmlToImage(template: string, credentialId: string): Promise { const browser = await puppeteer.launch({ - headless:'new' + executablePath: '/usr/bin/google-chrome', + args: ['--no-sandbox', '--disable-setuid-sandbox'], + headless: true }); const page = await browser.newPage(); - + await page.setViewport({ width: 1920, height: 1080 }); await page.setContent(template); const screenshot = await page.screenshot(); await browser.close(); diff --git a/apps/user/templates/winner-template.ts b/apps/user/templates/winner-template.ts index 8266ad6d4..49791fb1d 100644 --- a/apps/user/templates/winner-template.ts +++ b/apps/user/templates/winner-template.ts @@ -7,7 +7,7 @@ export class WinnerTemplate { async getWinnerTemplate(attributes: Attribute[]): Promise { try { - const [name, country, position, issuedBy, category, date] = await Promise.all(attributes).then((attributes) => { + const [name, issuedBy, date] = await Promise.all(attributes).then((attributes) => { const name = this.findAttributeByName(attributes, 'full_name')?.full_name ?? ''; const country = this.findAttributeByName(attributes, 'country')?.country ?? ''; const position = this.findAttributeByName(attributes, 'position')?.position ?? ''; @@ -19,82 +19,65 @@ export class WinnerTemplate { return ` - - - Certificate of Achievement - + #textOverlay { + position: absolute; + top: 280px; + left: 50%; + transform: translateX(-50%); + text-align: center; + color: #2B2B2A; + font-size: 24px; + + } + -
-
🏆
-

Certificate of Achievement

- -

${name}

-

has demonstrated outstanding performance and successfully completed the requirements for

-

Winner

-

Position: ${position}

-

Issued by: ${issuedBy}

- -
- -

Country: ${country}

-

Category: ${category}

- -

Issued Date: ${date}

- -

Congratulations!

+
+ background +
+
+

CERTIFICATE

+

OF ACHIEVEMENT

+ +

IS PROUDLY PRESENTED TO

+

${name}

+ + You are the winner of our contest + ${issuedBy} World Memory Championship 2023. +

+

+

We acknowledge your dedication, hard work, and

+

exceptional memory skills demonstrated during the competition.

+

+
${date} | Place: Cidco Exhibition Centre, Navi Mumbai, India
+
+
+ `; } catch {} diff --git a/package.json b/package.json index dd0264575..426b0aef1 100755 --- a/package.json +++ b/package.json @@ -68,6 +68,7 @@ "generate-password": "^1.7.0", "helmet": "^7.0.0", "html-pdf": "^3.0.1", + "html-to-image": "^1.11.11", "json2csv": "^5.0.7", "jsonwebtoken": "^9.0.1", "jwks-rsa": "^3.0.1", @@ -78,6 +79,7 @@ "nats": "^2.15.1", "nestjs-supabase-auth": "^1.0.9", "nestjs-typeorm-paginate": "^4.0.4", + "node-html-to-image": "^4.0.0", "node-qpdf2": "^2.0.0", "papaparse": "^5.4.1", "passport": "^0.6.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8f83b9449..c27e209ab 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -131,6 +131,9 @@ dependencies: html-pdf: specifier: ^3.0.1 version: 3.0.1 + html-to-image: + specifier: ^1.11.11 + version: 1.11.11 json2csv: specifier: ^5.0.7 version: 5.0.7 @@ -161,6 +164,9 @@ dependencies: nestjs-typeorm-paginate: specifier: ^4.0.4 version: 4.0.4(@nestjs/common@10.2.8)(typeorm@0.3.10) + node-html-to-image: + specifier: ^4.0.0 + version: 4.0.0 node-qpdf2: specifier: ^2.0.0 version: 2.0.0 @@ -1624,6 +1630,22 @@ packages: resolution: {integrity: sha512-NV/4nVNWFZSJCCIA3HIFJbbDKO/NARc9ej0tX5S9k2EVbkrFJC4Xt9b0u4rNZWL4V+F5LAjvta8vzEUw0rw+HA==} requiresBuild: true + /@puppeteer/browsers@1.5.0: + resolution: {integrity: sha512-za318PweGINh5LnHSph7C4xhs0tmRjCD8EPpzcKlw4nzSPhnULj+LTG3+TGefZvW1ti5gjw2JkdQvQsivBeZlg==} + engines: {node: '>=16.3.0'} + hasBin: true + dependencies: + debug: 4.3.4 + extract-zip: 2.0.1 + progress: 2.0.3 + proxy-agent: 6.3.0 + tar-fs: 3.0.4 + unbzip2-stream: 1.4.3 + yargs: 17.7.1 + transitivePeerDependencies: + - supports-color + dev: false + /@puppeteer/browsers@1.8.0: resolution: {integrity: sha512-TkRHIV6k2D8OlUe8RtG+5jgOF/H98Myx0M6AOafC8DdNVOFiBSFa5cpRDtpm8LXOa9sVwe0+e6Q3FC56X/DZfg==} engines: {node: '>=16.3.0'} @@ -1749,7 +1771,7 @@ packages: /@swc/helpers@0.3.17: resolution: {integrity: sha512-tb7Iu+oZ+zWJZ3HJqwx8oNwSDIU440hmVMDPhpACWQWnrZHK99Bxs70gT1L2dnr5Hg50ZRWEFkQCAnOVVV0z1Q==} dependencies: - tslib: 2.6.1 + tslib: 2.6.2 dev: false /@tootallnate/quickjs-emscripten@0.23.0: @@ -3157,6 +3179,15 @@ packages: engines: {node: '>=6.0'} dev: true + /chromium-bidi@0.4.20(devtools-protocol@0.0.1147663): + resolution: {integrity: sha512-ruHgVZFEv00mAQMz1tQjfjdG63jiPWrQPF6HLlX2ucqLqVTJoWngeBEKHaJ6n1swV/HSvgnBNbtTRIlcVyW3Fw==} + peerDependencies: + devtools-protocol: '*' + dependencies: + devtools-protocol: 0.0.1147663 + mitt: 3.0.1 + dev: false + /chromium-bidi@0.4.33(devtools-protocol@0.0.1203626): resolution: {integrity: sha512-IxoFM5WGQOIAd95qrSXzJUv4eXIrh+RvU3rwwqIiwYuvfE7U/Llj4fejbsJnjJMUYCuGtVQsY2gv7oGl4aTNSQ==} peerDependencies: @@ -3437,6 +3468,16 @@ packages: yaml: 1.10.2 dev: true + /cosmiconfig@8.2.0: + resolution: {integrity: sha512-3rTMnFJA1tCOPwRxtgF4wd7Ab2qvDbL8jX+3smjIbS4HlZBagTlpERbdN7iAbWlrfxE3M8c27kTwTawQ7st+OQ==} + engines: {node: '>=14'} + dependencies: + import-fresh: 3.3.0 + js-yaml: 4.1.0 + parse-json: 5.2.0 + path-type: 4.0.0 + dev: false + /cosmiconfig@8.3.6(typescript@5.1.6): resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==} engines: {node: '>=14'} @@ -3669,6 +3710,10 @@ packages: engines: {node: '>=8'} dev: true + /devtools-protocol@0.0.1147663: + resolution: {integrity: sha512-hyWmRrexdhbZ1tcJUGpO95ivbRhWXz++F4Ko+n21AY5PNln2ovoJw+8ZMNDTtip+CNFQfrtLVh/w4009dXO/eQ==} + dev: false + /devtools-protocol@0.0.1203626: resolution: {integrity: sha512-nEzHZteIUZfGCZtTiS1fRpC8UZmsfD1SiyPvaUNvS13dvKf666OAm8YTi0+Ca3n1nLEyu49Cy4+dPWpaHFJk9g==} dev: false @@ -4920,6 +4965,19 @@ packages: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} dev: true + /handlebars@4.7.8: + resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==} + engines: {node: '>=0.4.7'} + hasBin: true + dependencies: + minimist: 1.2.8 + neo-async: 2.6.2 + source-map: 0.6.1 + wordwrap: 1.0.0 + optionalDependencies: + uglify-js: 3.17.4 + dev: false + /har-schema@2.0.0: resolution: {integrity: sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==} engines: {node: '>=4'} @@ -5021,6 +5079,10 @@ packages: - supports-color dev: false + /html-to-image@1.11.11: + resolution: {integrity: sha512-9gux8QhvjRO/erSnDPv28noDZcPZmYE7e1vFsBLKLlRlKDSqNJYebj6Qz1TGd5lsRV+X+xYyjCKjuZdABinWjA==} + dev: false + /http-errors@2.0.0: resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} engines: {node: '>= 0.8'} @@ -6310,11 +6372,6 @@ packages: wrap-ansi: 6.2.0 dev: true - /lru-cache@10.0.0: - resolution: {integrity: sha512-svTf/fzsKHffP42sujkO/Rjs37BCIsQVRCeNYIm9WN8rgT7ffoUnRtZCqU+6BqcSBdv8gwJeTz8knJpgACeQMw==} - engines: {node: 14 || >=16.14} - dev: true - /lru-cache@10.0.1: resolution: {integrity: sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==} engines: {node: 14 || >=16.14} @@ -6623,7 +6680,6 @@ packages: /neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} - dev: true /nestjs-supabase-auth@1.0.9: resolution: {integrity: sha512-1Aar5K2WuGggPV8q/xzJCIeAQz5wkPcvKGLPTUXwt1he1EKLg+OdWK2C0T7LzTgO4uX2WLakNWZBsUTZEOvt4Q==} @@ -6691,6 +6747,19 @@ packages: hasBin: true dev: false + /node-html-to-image@4.0.0: + resolution: {integrity: sha512-lB8fkRleAKG4afJ2Wr7qJzIA5+//ue9OEoz+BMxQsowriGKR8sf4j4lK/pIXKakYwf/3aZHoDUNgOXuJ4HOzYA==} + dependencies: + handlebars: 4.7.8 + puppeteer: 21.0.1 + puppeteer-cluster: 0.23.0(puppeteer@21.0.1) + transitivePeerDependencies: + - bufferutil + - encoding + - supports-color + - utf-8-validate + dev: false + /node-int64@0.4.0: resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} dev: true @@ -7031,7 +7100,7 @@ packages: resolution: {integrity: sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==} engines: {node: '>=16 || 14 >=14.17'} dependencies: - lru-cache: 10.0.0 + lru-cache: 10.0.1 minipass: 7.0.2 dev: true @@ -7309,6 +7378,22 @@ packages: forwarded: 0.2.0 ipaddr.js: 1.9.1 + /proxy-agent@6.3.0: + resolution: {integrity: sha512-0LdR757eTj/JfuU7TL2YCuAZnxWXu3tkJbg4Oq3geW/qFNT/32T0sp2HnZ9O0lMR4q3vwAt0+xCA8SR0WAD0og==} + engines: {node: '>= 14'} + dependencies: + agent-base: 7.1.0 + debug: 4.3.4 + http-proxy-agent: 7.0.0 + https-proxy-agent: 7.0.2 + lru-cache: 7.18.3 + pac-proxy-agent: 7.0.1 + proxy-from-env: 1.1.0 + socks-proxy-agent: 8.0.2 + transitivePeerDependencies: + - supports-color + dev: false + /proxy-agent@6.3.1: resolution: {integrity: sha512-Rb5RVBy1iyqOtNl15Cw/llpeLH8bsb37gM1FUfKQ+Wck6xHlbAhWGUFiTRHtkjqGTA5pSHz6+0hrPW/oECihPQ==} engines: {node: '>= 14'} @@ -7353,6 +7438,34 @@ packages: resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==} engines: {node: '>=6'} + /puppeteer-cluster@0.23.0(puppeteer@21.0.1): + resolution: {integrity: sha512-108terIWDzPrQopmoYSPd5yDoy3FGJ2dNnoGMkGYPs6xtkdhgaECwpfZkzaRToMQPZibUOz0/dSSGgPEdXEhkQ==} + peerDependencies: + puppeteer: '>=1.5.0' + dependencies: + debug: 4.3.4 + puppeteer: 21.0.1 + transitivePeerDependencies: + - supports-color + dev: false + + /puppeteer-core@21.0.1: + resolution: {integrity: sha512-E8eWLGhaZZpa7dYe/58qGX7SLb4mTg42NP5M7B+ibPrncgNjTOQa9x1sFIlTn1chF/BmoZqOcMIvwuxcb/9XzQ==} + engines: {node: '>=16.3.0'} + dependencies: + '@puppeteer/browsers': 1.5.0 + chromium-bidi: 0.4.20(devtools-protocol@0.0.1147663) + cross-fetch: 4.0.0 + debug: 4.3.4 + devtools-protocol: 0.0.1147663 + ws: 8.13.0 + transitivePeerDependencies: + - bufferutil + - encoding + - supports-color + - utf-8-validate + dev: false + /puppeteer-core@21.5.0: resolution: {integrity: sha512-qG0RJ6qKgFz09UUZxDB9IcyTJGypQXMuE8WmEoHk7kgjutmRiOVv5RgsyUkY67AxDdBWx21bn1PHHRJnO/6b4A==} engines: {node: '>=16.3.0'} @@ -7370,6 +7483,21 @@ packages: - utf-8-validate dev: false + /puppeteer@21.0.1: + resolution: {integrity: sha512-KTjmSdPZ6bMkq3EbAzAUhcB3gMDXvdwd6912rxG9hNtjwRJzHSA568vh6vIbO2WQeNmozRdt1LtiUMLSWfeMrg==} + engines: {node: '>=16.3.0'} + requiresBuild: true + dependencies: + '@puppeteer/browsers': 1.5.0 + cosmiconfig: 8.2.0 + puppeteer-core: 21.0.1 + transitivePeerDependencies: + - bufferutil + - encoding + - supports-color + - utf-8-validate + dev: false + /puppeteer@21.5.0(typescript@5.1.6): resolution: {integrity: sha512-prvy9rdauyIaaEgefQRcw9zhQnYQbl8O1Gj5VJazKJ7kwNx703+Paw/1bwA+b96jj/S+r55hrmF5SfiEG5PUcg==} engines: {node: '>=16.3.0'} @@ -8746,6 +8874,14 @@ packages: engines: {node: '>=14.17'} hasBin: true + /uglify-js@3.17.4: + resolution: {integrity: sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==} + engines: {node: '>=0.8.0'} + hasBin: true + requiresBuild: true + dev: false + optional: true + /uid@2.0.2: resolution: {integrity: sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==} engines: {node: '>=8'} @@ -9148,6 +9284,10 @@ packages: execa: 4.1.0 dev: true + /wordwrap@1.0.0: + resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} + dev: false + /wrap-ansi@6.2.0: resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} engines: {node: '>=8'} @@ -9187,6 +9327,19 @@ packages: utf-8-validate: optional: true + /ws@8.13.0: + resolution: {integrity: sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + dev: false + /ws@8.14.2: resolution: {integrity: sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==} engines: {node: '>=10.0.0'} @@ -9318,6 +9471,19 @@ packages: yargs-parser: 20.2.9 dev: false + /yargs@17.7.1: + resolution: {integrity: sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw==} + engines: {node: '>=12'} + dependencies: + cliui: 8.0.1 + escalade: 3.1.1 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + dev: false + /yargs@17.7.2: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} From 81989b85935acd0c427635af5cc5f2518b73fa22 Mon Sep 17 00:00:00 2001 From: KulkarniShashank Date: Tue, 21 Nov 2023 16:25:43 +0530 Subject: [PATCH 53/62] fix: solved the bug for connection label on shared agent Signed-off-by: KulkarniShashank --- apps/agent-service/src/agent-service.service.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/agent-service/src/agent-service.service.ts b/apps/agent-service/src/agent-service.service.ts index 89d717489..94e54931d 100644 --- a/apps/agent-service/src/agent-service.service.ts +++ b/apps/agent-service/src/agent-service.service.ts @@ -553,6 +553,9 @@ export class AgentServiceService { ledgerId: payload.ledgerId }; + const getOrgAgent = await this.agentServiceRepository.getOrgDetails(payload.orgId); + this.logger.log(`getOrgAgent::: ${JSON.stringify(getOrgAgent)}`); + if (payload.clientSocketId) { socket.emit('agent-spinup-process-completed', { clientId: payload.clientSocketId }); } @@ -563,7 +566,7 @@ export class AgentServiceService { socket.emit('invitation-url-creation-started', { clientId: payload.clientSocketId }); } - await this._createLegacyConnectionInvitation(payload.orgId, user, storeOrgAgentData.walletName); + await this._createLegacyConnectionInvitation(payload.orgId, user, getOrgAgent.name); if (payload.clientSocketId) { socket.emit('invitation-url-creation-success', { clientId: payload.clientSocketId }); From 2394ee595e74883e39a8cadaa40ecb6dbcac6cd1 Mon Sep 17 00:00:00 2001 From: KulkarniShashank Date: Tue, 21 Nov 2023 16:51:36 +0530 Subject: [PATCH 54/62] fix: error handling on the bulk issuance Signed-off-by: KulkarniShashank --- apps/issuance/src/issuance.service.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/apps/issuance/src/issuance.service.ts b/apps/issuance/src/issuance.service.ts index c9d33d148..01d561ea1 100644 --- a/apps/issuance/src/issuance.service.ts +++ b/apps/issuance/src/issuance.service.ts @@ -43,7 +43,7 @@ export class IssuanceService { @InjectQueue('bulk-issuance') private bulkIssuanceQueue: Queue ) { } - + async sendCredentialCreateOffer(orgId: number, user: IUserRequest, credentialDefinitionId: string, comment: string, connectionId: string, attributes: object[]): Promise { try { const agentDetails = await this.issuanceRepository.getAgentEndPoint(orgId); @@ -377,7 +377,7 @@ export class IssuanceService { return allSuccessful; } catch (error) { this.logger.error(`[outOfBoundCredentialOffer] - error in create out-of-band credentials: ${JSON.stringify(error)}`); - throw new RpcException(error); + throw new RpcException(error.response ? error.response : error); } } @@ -831,7 +831,7 @@ export class IssuanceService { autoConnect: true, transports: ['websocket'] }); - + const fileUploadData: FileUploadData = { fileUpload: '', fileRow: '', @@ -874,7 +874,7 @@ export class IssuanceService { } } catch (error) { this.logger.error( - `error in issuanceBulkCredential for data ${jobDetails} : ${error}` + `error in issuanceBulkCredential for data ${JSON.stringify(jobDetails)} : ${JSON.stringify(error)}` ); fileUploadData.isError = true; fileUploadData.error = error.message; @@ -903,6 +903,7 @@ export class IssuanceService { } } catch (error) { this.logger.error(`Error completing bulk issuance process: ${error}`); + throw error; } } From 8e2a29f7612524890d52c7eb5760276a367e66a7 Mon Sep 17 00:00:00 2001 From: KulkarniShashank Date: Tue, 21 Nov 2023 17:10:39 +0530 Subject: [PATCH 55/62] fix: error handling on the bulk issuance process Signed-off-by: KulkarniShashank --- apps/issuance/src/issuance.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/issuance/src/issuance.service.ts b/apps/issuance/src/issuance.service.ts index 01d561ea1..ffa13861c 100644 --- a/apps/issuance/src/issuance.service.ts +++ b/apps/issuance/src/issuance.service.ts @@ -877,7 +877,7 @@ export class IssuanceService { `error in issuanceBulkCredential for data ${JSON.stringify(jobDetails)} : ${JSON.stringify(error)}` ); fileUploadData.isError = true; - fileUploadData.error = error.message; + fileUploadData.error = error.error; fileUploadData.detailError = `${JSON.stringify(error)}`; } await this.issuanceRepository.updateFileUploadData(fileUploadData); From f006e1c2f35285757c601556b5c30106cc7ded7c Mon Sep 17 00:00:00 2001 From: KulkarniShashank Date: Tue, 21 Nov 2023 17:14:55 +0530 Subject: [PATCH 56/62] fix: error handling on the bulk issuance process Signed-off-by: KulkarniShashank --- apps/issuance/src/issuance.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/issuance/src/issuance.service.ts b/apps/issuance/src/issuance.service.ts index ffa13861c..ebabc1ac7 100644 --- a/apps/issuance/src/issuance.service.ts +++ b/apps/issuance/src/issuance.service.ts @@ -877,7 +877,7 @@ export class IssuanceService { `error in issuanceBulkCredential for data ${JSON.stringify(jobDetails)} : ${JSON.stringify(error)}` ); fileUploadData.isError = true; - fileUploadData.error = error.error; + fileUploadData.error = error.error ? error.error : error; fileUploadData.detailError = `${JSON.stringify(error)}`; } await this.issuanceRepository.updateFileUploadData(fileUploadData); From 38178782e91f62d0e1fa1c2224b8bdc1642a6da8 Mon Sep 17 00:00:00 2001 From: KulkarniShashank Date: Tue, 21 Nov 2023 17:35:29 +0530 Subject: [PATCH 57/62] fix: error handling on the bulk issuance process Signed-off-by: KulkarniShashank --- apps/issuance/src/issuance.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/issuance/src/issuance.service.ts b/apps/issuance/src/issuance.service.ts index ebabc1ac7..10c70544b 100644 --- a/apps/issuance/src/issuance.service.ts +++ b/apps/issuance/src/issuance.service.ts @@ -877,7 +877,7 @@ export class IssuanceService { `error in issuanceBulkCredential for data ${JSON.stringify(jobDetails)} : ${JSON.stringify(error)}` ); fileUploadData.isError = true; - fileUploadData.error = error.error ? error.error : error; + fileUploadData.error = JSON.stringify(error.error) ? JSON.stringify(error.error) : JSON.stringify(error); fileUploadData.detailError = `${JSON.stringify(error)}`; } await this.issuanceRepository.updateFileUploadData(fileUploadData); From f2ee22b4df47c4213473ab5caa1765bba342ca16 Mon Sep 17 00:00:00 2001 From: Nishad Date: Tue, 21 Nov 2023 19:23:01 +0530 Subject: [PATCH 58/62] fixed file upload blob Signed-off-by: Nishad --- apps/api-gateway/src/issuance/issuance.controller.ts | 2 +- apps/issuance/src/issuance.repository.ts | 5 ++++- apps/issuance/src/issuance.service.ts | 7 ++----- libs/common/src/response-messages/index.ts | 6 +++--- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/apps/api-gateway/src/issuance/issuance.controller.ts b/apps/api-gateway/src/issuance/issuance.controller.ts index b701d88c7..5f32db861 100644 --- a/apps/api-gateway/src/issuance/issuance.controller.ts +++ b/apps/api-gateway/src/issuance/issuance.controller.ts @@ -245,7 +245,7 @@ export class IssuanceController { const reqPayload: RequestPayload = { credDefId: credentialDefinitionId, fileKey, - fileName: file?.originalname + fileName: file?.filename || file?.originalname }; this.logger.log(`reqPayload::::::${JSON.stringify(reqPayload)}`); const importCsvDetails = await this.issueCredentialService.importCsv( diff --git a/apps/issuance/src/issuance.repository.ts b/apps/issuance/src/issuance.repository.ts index fa2ee19c4..ede30505c 100644 --- a/apps/issuance/src/issuance.repository.ts +++ b/apps/issuance/src/issuance.repository.ts @@ -278,7 +278,10 @@ export class IssuanceRepository { ] }, take: Number(getAllfileDetails?.pageSize), - skip: (getAllfileDetails?.pageNumber - 1) * getAllfileDetails?.pageSize + skip: (getAllfileDetails?.pageNumber - 1) * getAllfileDetails?.pageSize, + orderBy: { + createDateTime: 'desc' + } }); const fileListWithDetails = await Promise.all( diff --git a/apps/issuance/src/issuance.service.ts b/apps/issuance/src/issuance.service.ts index c9d33d148..36c80818b 100644 --- a/apps/issuance/src/issuance.service.ts +++ b/apps/issuance/src/issuance.service.ts @@ -486,11 +486,8 @@ export class IssuanceService { const filePath = join(process.cwd(), `uploadedFiles/exports`); - - let processedFileName: string = credentialDefinitionId; - processedFileName = processedFileName.replace(/[\/:*?"<>|]/g, '_'); const timestamp = Math.floor(Date.now() / 1000); - const fileName = `${processedFileName}-${timestamp}.csv`; + const fileName = `${schemaResponse.tag}-${timestamp}.csv`; await createFile(filePath, fileName, csv); this.logger.log(`File created - ${fileName}`); @@ -504,7 +501,7 @@ export class IssuanceService { const filePathToDownload = `${process.env.API_GATEWAY_PROTOCOL_SECURE}://${process.env.UPLOAD_LOGO_HOST}/${fileName}`; return { fileContent: filePathToDownload, - fileName: processedFileName + fileName }; } catch (error) { throw new Error('An error occurred during CSV export.'); diff --git a/libs/common/src/response-messages/index.ts b/libs/common/src/response-messages/index.ts index 29a32866d..164e91996 100644 --- a/libs/common/src/response-messages/index.ts +++ b/libs/common/src/response-messages/index.ts @@ -163,11 +163,11 @@ export const ResponseMessages = { }, issuance: { success: { - create: 'Issue-credential offer created successfully', - fetch: 'Issue-credential fetched successfully', + create: 'Credentials offer created successfully', + fetch: 'Credentials fetched successfully', importCSV: 'File imported sucessfully', previewCSV: 'File details fetched sucessfully', - bulkIssuance: 'Bulk-issunace process started', + bulkIssuance: 'Issunace process started. It will take some time', notFound: 'Schema records not found' }, error: { From bc192c4b5229dac4fbdda1941481fe556721fc4d Mon Sep 17 00:00:00 2001 From: Nishad Date: Tue, 21 Nov 2023 19:34:54 +0530 Subject: [PATCH 59/62] sorted the bulk issuance file data Signed-off-by: Nishad --- apps/issuance/src/issuance.repository.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/issuance/src/issuance.repository.ts b/apps/issuance/src/issuance.repository.ts index ede30505c..1edcabbd4 100644 --- a/apps/issuance/src/issuance.repository.ts +++ b/apps/issuance/src/issuance.repository.ts @@ -337,7 +337,10 @@ export class IssuanceRepository { ] }, take: Number(getAllfileDetails?.pageSize), - skip: (getAllfileDetails?.pageNumber - 1) * getAllfileDetails?.pageSize + skip: (getAllfileDetails?.pageNumber - 1) * getAllfileDetails?.pageSize, + orderBy: { + createDateTime: 'desc' + } }); const fileCount = await this.prisma.file_data.count({ where: { From 702ad498cf48a771f10bc1b43581d8fb3e69ebf8 Mon Sep 17 00:00:00 2001 From: KulkarniShashank Date: Tue, 21 Nov 2023 21:29:45 +0530 Subject: [PATCH 60/62] fix: Added the agent protocol on agent-service Signed-off-by: KulkarniShashank --- apps/agent-service/src/agent-service.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/agent-service/src/agent-service.service.ts b/apps/agent-service/src/agent-service.service.ts index 94e54931d..3b0bce14c 100644 --- a/apps/agent-service/src/agent-service.service.ts +++ b/apps/agent-service/src/agent-service.service.ts @@ -268,7 +268,7 @@ export class AgentServiceService { return agentSpinUpResponse.then(async (agentDetails) => { if (agentDetails) { const controllerEndpoints = JSON.parse(agentDetails); - const agentEndPoint = `${process.env.API_GATEWAY_PROTOCOL}://${controllerEndpoints.CONTROLLER_ENDPOINT}`; + const agentEndPoint = `${process.env.AGENT_PROTOCOL}://${controllerEndpoints.CONTROLLER_ENDPOINT}`; if (agentEndPoint && agentSpinupDto.clientSocketId) { const socket = io(`${process.env.SOCKET_HOST}`, { From 4b1082786a3690a47cd1ba20f3c3ce42b840f4cc Mon Sep 17 00:00:00 2001 From: KulkarniShashank Date: Tue, 21 Nov 2023 21:32:05 +0530 Subject: [PATCH 61/62] Refactor error message Signed-off-by: KulkarniShashank --- libs/common/src/response-messages/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/common/src/response-messages/index.ts b/libs/common/src/response-messages/index.ts index 164e91996..7a305fed1 100644 --- a/libs/common/src/response-messages/index.ts +++ b/libs/common/src/response-messages/index.ts @@ -167,7 +167,7 @@ export const ResponseMessages = { fetch: 'Credentials fetched successfully', importCSV: 'File imported sucessfully', previewCSV: 'File details fetched sucessfully', - bulkIssuance: 'Issunace process started. It will take some time', + bulkIssuance: 'Issuance process started. It will take some time', notFound: 'Schema records not found' }, error: { From 02d9d103064756fe0f388ff43f7258835c7009dd Mon Sep 17 00:00:00 2001 From: bhavanakarwade Date: Tue, 21 Nov 2023 23:01:29 +0530 Subject: [PATCH 62/62] refactor: developed new html templates Signed-off-by: bhavanakarwade --- apps/api-gateway/src/user/user.controller.ts | 8 +- apps/user/src/user.service.ts | 11 +- apps/user/templates/arbiter-template.ts | 105 +++++++++++---- apps/user/templates/participant-template.ts | 120 +++++++++++------- apps/user/templates/winner-template.ts | 30 +++-- apps/user/templates/world-record-template.ts | 102 ++++++++++++--- libs/aws/src/aws.service.ts | 9 +- .../src/interfaces/response.interface.ts | 1 + libs/enum/src/enum.ts | 2 +- 9 files changed, 280 insertions(+), 108 deletions(-) diff --git a/apps/api-gateway/src/user/user.controller.ts b/apps/api-gateway/src/user/user.controller.ts index a4ff3a898..764580a9e 100644 --- a/apps/api-gateway/src/user/user.controller.ts +++ b/apps/api-gateway/src/user/user.controller.ts @@ -310,12 +310,16 @@ export class UserController { async shareUserCertificate( @Body() shareUserCredentials: CreateUserCertificateDto, @Res() res: Response - ): Promise { - const imageBuffer = await this.userService.shareUserCertificate(shareUserCredentials); + ): Promise { + const schemaIdParts = shareUserCredentials.schemaId.split(':'); + // eslint-disable-next-line prefer-destructuring + const title = schemaIdParts[2]; + const imageBuffer = await this.userService.shareUserCertificate(shareUserCredentials); const finalResponse: IResponseType = { statusCode: HttpStatus.CREATED, message: 'Certificate url generated successfully', + label: title, data: imageBuffer.response }; return res.status(HttpStatus.CREATED).json(finalResponse); diff --git a/apps/user/src/user.service.ts b/apps/user/src/user.service.ts index dd4dd3bdc..6385ae49c 100644 --- a/apps/user/src/user.service.ts +++ b/apps/user/src/user.service.ts @@ -50,6 +50,7 @@ import validator from 'validator'; import { DISALLOWED_EMAIL_DOMAIN } from '@credebl/common/common.constant'; import { AwsService } from '@credebl/aws'; import puppeteer from 'puppeteer'; +import { WorldRecordTemplate } from '../templates/world-record-template'; @Injectable() export class UserService { @@ -578,6 +579,11 @@ export class UserService { const userArbiterTemplate = new ArbiterTemplate(); template = await userArbiterTemplate.getArbiterTemplate(attributeArray); break; + case UserCertificateId.WORLD_RECORD: + // eslint-disable-next-line no-case-declarations + const userWorldRecordTemplate = new WorldRecordTemplate(); + template = await userWorldRecordTemplate.getWorldRecordTemplate(attributeArray); + break; default: throw new NotFoundException('error in get attributes'); } @@ -588,7 +594,7 @@ export class UserService { const imageUrl = await this.awsService.uploadUserCertificate( imageBuffer, - 'jpeg', + 'png', verifyCode, 'certificates', 'base64' @@ -606,6 +612,7 @@ export class UserService { } return `${process.env.FRONT_END_URL}/certificates/${shareUserCertificate.credentialId}`; + } async saveCertificateUrl(imageUrl: string, credentialId: string): Promise { @@ -619,7 +626,7 @@ export class UserService { headless: true }); const page = await browser.newPage(); - await page.setViewport({ width: 1920, height: 1080 }); + await page.setViewport({ width: 800, height: 1020, deviceScaleFactor: 6}); await page.setContent(template); const screenshot = await page.screenshot(); await browser.close(); diff --git a/apps/user/templates/arbiter-template.ts b/apps/user/templates/arbiter-template.ts index b2be9e502..d2bfd4c73 100644 --- a/apps/user/templates/arbiter-template.ts +++ b/apps/user/templates/arbiter-template.ts @@ -1,27 +1,88 @@ -import { Attribute } from "../interfaces/user.interface"; +import { Attribute } from '../interfaces/user.interface'; export class ArbiterTemplate { - public getArbiterTemplate(attributes: Attribute[]): string { - const name = 0 < attributes.length ? attributes[0].name : ''; + findAttributeByName(attributes: Attribute[], name: string): Attribute { + return attributes.find((attr) => name in attr); + } - try { - return ` - - - - - Arbiter Template - - -
-
👩‍⚖️
-

Thank You, ${name}!

-

Your role as ${attributes} is essential in our contest.

-
- - `; - } catch (error) { + async getArbiterTemplate(attributes: Attribute[]): Promise { + try { + const [name, country, issuedBy] = await Promise.all(attributes).then((attributes) => { + const name = this.findAttributeByName(attributes, 'full_name')?.full_name ?? ''; + const country = this.findAttributeByName(attributes, 'country')?.country ?? ''; + const issuedBy = this.findAttributeByName(attributes, 'issued_by')?.issued_by ?? ''; + return [name, country, issuedBy]; + }); + return ` + + + + + + + + + + +
+ background +
+
+

CERTIFICATE

+

OF RECOGNITION

+
- } - } +

IS PROUDLY PRESENTED TO

+

${name}

+ + +

has served as an Arbiter at the + ${issuedBy} World Memory Championship 2023. +

+

+

+

Your dedication, professionalism, and impartiality as an Arbiter

+

have significantly contributed to the fair and smooth conduct

+

of the championship. Your commitment to upholding the

+

highest standards of integrity and sportsmanship has

+

played a crucial role in maintaining the credibility of the competition.

+ +

+
Date: 24, 25, 26 November 2023 | Place: Cidco Exhibition Centre, Navi Mumbai, ${country}
+
+
+ + + `; + } catch {} + } } diff --git a/apps/user/templates/participant-template.ts b/apps/user/templates/participant-template.ts index 0b372658c..b040fbb70 100644 --- a/apps/user/templates/participant-template.ts +++ b/apps/user/templates/participant-template.ts @@ -1,54 +1,84 @@ import { Attribute } from "../interfaces/user.interface"; export class ParticipantTemplate { - public getParticipantTemplate(attributes: Attribute[]): string { - try { - const nameAttribute = attributes.find(attr => 'full_name' in attr); - const countryAttribute = attributes.find(attr => 'country' in attr); - const positionAttribute = attributes.find(attr => 'position' in attr); - const issuedByAttribute = attributes.find(attr => 'issued_by' in attr); - const categoryAttribute = attributes.find(attr => 'category' in attr); - const dateAttribute = attributes.find(attr => 'issued_date' in attr); - - const name = nameAttribute ? nameAttribute['full_name'] : ''; - const country = countryAttribute ? countryAttribute['country'] : ''; - const position = positionAttribute ? positionAttribute['position'] : ''; - const issuedBy = issuedByAttribute ? issuedByAttribute['issued_by'] : ''; - const category = categoryAttribute ? categoryAttribute['category'] : ''; - const date = dateAttribute ? dateAttribute['issued_date'] : ''; - - return ` - - - - - Certificate of Achievement - - - -
-
🏆
-

Certificate of Achievement

- -

${name}

-

has demonstrated outstanding performance and successfully completed the requirements for

- -

Participant

-

Position: ${position}

-

Issued by: ${issuedBy}

- -
- -

Country: ${country}

-

Category: ${category}

+ findAttributeByName(attributes: Attribute[], name: string): Attribute { + return attributes.find((attr) => name in attr); + } -

Issued Date: ${date}

+ async getParticipantTemplate(attributes: Attribute[]): Promise { -

Congratulations!

-
+ try { + const [name, country, issuedBy] = await Promise.all(attributes).then((attributes) => { + const name = this.findAttributeByName(attributes, 'full_name')?.full_name ?? ''; + const country = this.findAttributeByName(attributes, 'country')?.country ?? ''; + const issuedBy = this.findAttributeByName(attributes, 'issued_by')?.issued_by ?? ''; + return [name, country, issuedBy]; + }); - - `; + return ` + + + + + + + + + + +
+ background +
+
+

CERTIFICATE

+

OF ACHIEVEMENT

+
+ +

IS PROUDLY PRESENTED TO

+

${name}

+ + for successfully participating in the + ${issuedBy} World Memory Championship 2023. +

+

+

We acknowledge your dedication, hard work, and

+

exceptional memory skills demonstrated during the competition.

+

+
Date: 24, 25, 26 November 2023 | Place: Cidco Exhibition Centre, Navi Mumbai, ${country}
+
+
+ + + `; } catch (error) {} } } diff --git a/apps/user/templates/winner-template.ts b/apps/user/templates/winner-template.ts index 49791fb1d..150fe5a05 100644 --- a/apps/user/templates/winner-template.ts +++ b/apps/user/templates/winner-template.ts @@ -7,14 +7,15 @@ export class WinnerTemplate { async getWinnerTemplate(attributes: Attribute[]): Promise { try { - const [name, issuedBy, date] = await Promise.all(attributes).then((attributes) => { + const [name, country, position, discipline, issuedBy, category] = await Promise.all(attributes).then((attributes) => { const name = this.findAttributeByName(attributes, 'full_name')?.full_name ?? ''; const country = this.findAttributeByName(attributes, 'country')?.country ?? ''; const position = this.findAttributeByName(attributes, 'position')?.position ?? ''; + const discipline = this.findAttributeByName(attributes, 'discipline')?.discipline ?? ''; const issuedBy = this.findAttributeByName(attributes, 'issued_by')?.issued_by ?? ''; const category = this.findAttributeByName(attributes, 'category')?.category ?? ''; const date = this.findAttributeByName(attributes, 'issued_date')?.issued_date ?? ''; - return [name, country, position, issuedBy, category, date]; + return [name, country, position, discipline, issuedBy, category, date]; }); return ` @@ -41,7 +42,7 @@ export class WinnerTemplate { } #textOverlay { - position: absolute; + position: absolute; top: 280px; left: 50%; transform: translateX(-50%); @@ -55,31 +56,34 @@ export class WinnerTemplate {
- background -
+ background +

CERTIFICATE

-

OF ACHIEVEMENT

+

OF EXCELLENCE

IS PROUDLY PRESENTED TO

-

${name}

+

${name}

- You are the winner of our contest - ${issuedBy} World Memory Championship 2023. +

has secured ${position} position for ${discipline}

+

in ${category} category at the

+

+ ${issuedBy} World Memory Championship 2023. +

We acknowledge your dedication, hard work, and

exceptional memory skills demonstrated during the competition.

-
${date} | Place: Cidco Exhibition Centre, Navi Mumbai, India
+
Date: 24, 25, 26 November 2023 | Place: Cidco Exhibition Centre, Navi Mumbai, ${country}
- `; + + `; } catch {} } } diff --git a/apps/user/templates/world-record-template.ts b/apps/user/templates/world-record-template.ts index d07a062ad..1436b2348 100644 --- a/apps/user/templates/world-record-template.ts +++ b/apps/user/templates/world-record-template.ts @@ -1,22 +1,86 @@ +import { Attribute } from '../interfaces/user.interface'; + export class WorldRecordTemplate { - public getWorldReccordTemplate(): string { - return ` - - - - - - World Record - - -
-
- 🏆 -
-

Congratulations!

-

You're the Winner of our contest.

-
- - `; + findAttributeByName(attributes: Attribute[], name: string): Attribute { + return attributes.find((attr) => name in attr); + } + + async getWorldRecordTemplate(attributes: Attribute[]): Promise { + try { + const [name, country, discipline, issuedBy] = await Promise.all(attributes).then((attributes) => { + const name = this.findAttributeByName(attributes, 'full_name')?.full_name ?? ''; + const country = this.findAttributeByName(attributes, 'country')?.country ?? ''; + const discipline = this.findAttributeByName(attributes, 'discipline')?.discipline ?? ''; + const issuedBy = this.findAttributeByName(attributes, 'issued_by')?.issued_by ?? ''; + return [name, country, discipline, issuedBy]; + }); + return ` + + + + + + + + + + +
+ background +
+
+

CERTIFICATE

+

OF WORLD RECORD

+
+ +

IS PROUDLY PRESENTED TO

+

${name}

+ + for successfully creating the world record in the + ${discipline} +

discipline during the + ${issuedBy} World Memory Championship 2023. +

+

+

+

We acknowledge your dedication, hard work, and

+

exceptional memory skills demonstrated during the competition.

+

+
Date: 24, 25, 26 November 2023 | Place: Cidco Exhibition Centre, Navi Mumbai, ${country}
+
+
+ + + `; + } catch {} } } diff --git a/libs/aws/src/aws.service.ts b/libs/aws/src/aws.service.ts index 8e1dcbd72..619e32ebb 100644 --- a/libs/aws/src/aws.service.ts +++ b/libs/aws/src/aws.service.ts @@ -15,30 +15,31 @@ export class AwsService { region: process.env.AWS_REGION }); this.s4 = new S3({ + accessKeyId: process.env.AWS_PUBLIC_ACCESS_KEY, secretAccessKey: process.env.AWS_PUBLIC_SECRET_KEY, region: process.env.AWS_PUBLIC_REGION }); } - + async uploadUserCertificate( fileBuffer: Buffer, ext: string, verifyCode: string, pathAWS: string = '', encoding = 'base64', - filename: string = 'cerficate' + filename: string = 'certificate' ): Promise { const timestamp = Date.now(); const putObjectAsync = promisify(this.s4.putObject).bind(this.s4); - try { await putObjectAsync({ + Bucket: process.env.AWS_PUBLIC_BUCKET_NAME, Key: `${pathAWS}/${encodeURIComponent(filename)}.${timestamp}.${ext}`, Body: fileBuffer, ContentEncoding: encoding, - ContentType: `image/jpeg` + ContentType: `image/png` }); return `https://${process.env.AWS_PUBLIC_BUCKET_NAME}.s3.${process.env.AWS_PUBLIC_REGION}.amazonaws.com/${pathAWS}/${encodeURIComponent(filename)}.${timestamp}.${ext}`; } catch (err) { diff --git a/libs/common/src/interfaces/response.interface.ts b/libs/common/src/interfaces/response.interface.ts index 7a7211741..52639580f 100644 --- a/libs/common/src/interfaces/response.interface.ts +++ b/libs/common/src/interfaces/response.interface.ts @@ -1,6 +1,7 @@ export default interface IResponseType { statusCode: number; message?: string; + label?: string; data?: unknown; error?: unknown; }; diff --git a/libs/enum/src/enum.ts b/libs/enum/src/enum.ts index 37bd73879..729805fbf 100644 --- a/libs/enum/src/enum.ts +++ b/libs/enum/src/enum.ts @@ -42,5 +42,5 @@ export enum UserCertificateId { WINNER = 'Winner', PARTICIPANT = 'Participant', ARBITER = 'Arbiter', - WORLD_RECORD = 'WORLD_RECORD' + WORLD_RECORD = 'WorldRecord' }