diff --git a/.env.sample b/.env.sample index 43f96cc04..17873880c 100644 --- a/.env.sample +++ b/.env.sample @@ -48,6 +48,7 @@ PLATFORM_ID= AFJ_AGENT_ENDPOINT_PATH=/apps/agent-provisioning/AFJ/endpoints/ DATABASE_URL="postgresql://postgres:xxxxxx@localhost:5432/postgres?schema=public" #Provide supabase postgres url and Use the correct user/pwd, IP Address +POOL_DATABASE_URL="" #Provide pooler supabase postgres url CLUSTER_NAME="" # ecs cluster TESKDEFINITION_FAMILY="" # ecs task-definition AGENT_PROTOCOL=http diff --git a/.gitignore b/.gitignore index 77d23f742..b40d5736d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,5 @@ node_modules dist -uploadedFiles .env sonar-project.properties .scannerwork/* diff --git a/Dockerfiles/Dockerfile.api-gateway b/Dockerfiles/Dockerfile.api-gateway index 4dead3d51..7969439be 100644 --- a/Dockerfiles/Dockerfile.api-gateway +++ b/Dockerfiles/Dockerfile.api-gateway @@ -30,6 +30,7 @@ COPY --from=build /app/dist/apps/api-gateway/ ./dist/apps/api-gateway/ # Copy the libs folder from the build stage COPY --from=build /app/libs/ ./libs/ COPY --from=build /app/node_modules ./node_modules +COPY --from=build /app/uploadedFiles ./uploadedFiles # Set the command to run the microservice CMD ["sh", "-c", "cd libs/prisma-service && npx prisma migrate deploy && npx prisma generate && cd ../.. && node dist/apps/api-gateway/main.js"] diff --git a/Dockerfiles/Dockerfile.issuance b/Dockerfiles/Dockerfile.issuance index a1a4c06ed..9284bc21b 100644 --- a/Dockerfiles/Dockerfile.issuance +++ b/Dockerfiles/Dockerfile.issuance @@ -31,6 +31,7 @@ COPY --from=build /app/dist/apps/issuance/ ./dist/apps/issuance/ COPY --from=build /app/libs/ ./libs/ #COPY --from=build /app/package.json ./ COPY --from=build /app/node_modules ./node_modules +COPY --from=build /app/uploadedFiles ./uploadedFiles # Set the command to run the microservice diff --git a/apps/agent-service/src/agent-service.controller.ts b/apps/agent-service/src/agent-service.controller.ts index 1d4a99192..9b33fe4c8 100644 --- a/apps/agent-service/src/agent-service.controller.ts +++ b/apps/agent-service/src/agent-service.controller.ts @@ -1,7 +1,7 @@ import { Controller } from '@nestjs/common'; import { MessagePattern } from '@nestjs/microservices'; import { AgentServiceService } from './agent-service.service'; -import { GetCredDefAgentRedirection, GetSchemaAgentRedirection, IAgentSpinupDto, IIssuanceCreateOffer, ITenantCredDef, ITenantDto, ITenantSchema } from './interface/agent-service.interface'; +import { GetCredDefAgentRedirection, GetSchemaAgentRedirection, IAgentSpinupDto, IIssuanceCreateOffer, ITenantCredDef, ITenantDto, ITenantSchema, OutOfBandCredentialOffer } from './interface/agent-service.interface'; import { IConnectionDetails, IUserRequestInterface } from './interface/agent-service.interface'; import { ISendProofRequestPayload } from './interface/agent-service.interface'; import { user } from '@prisma/client'; @@ -124,4 +124,9 @@ export class AgentServiceController { async submitTransaction(payload: { url: string, apiKey: string, submitEndorsementPayload:object }): Promise { return this.agentServiceService.sumbitTransaction(payload.url, payload.apiKey, payload.submitEndorsementPayload); } + + @MessagePattern({ cmd: 'agent-out-of-band-credential-offer' }) + async outOfBandCredentialOffer(payload: { outOfBandIssuancePayload: OutOfBandCredentialOffer, url: string, apiKey: string }): Promise { + return this.agentServiceService.outOfBandCredentialOffer(payload.outOfBandIssuancePayload, payload.url, payload.apiKey); + } } diff --git a/apps/agent-service/src/agent-service.service.ts b/apps/agent-service/src/agent-service.service.ts index ae79f30d3..dc6adb010 100644 --- a/apps/agent-service/src/agent-service.service.ts +++ b/apps/agent-service/src/agent-service.service.ts @@ -16,7 +16,7 @@ import * as dotenv from 'dotenv'; import * as fs from 'fs'; import { catchError, map } from 'rxjs/operators'; dotenv.config(); -import { GetCredDefAgentRedirection, IAgentSpinupDto, IStoreOrgAgentDetails, ITenantCredDef, ITenantDto, ITenantSchema, IWalletProvision, ISendProofRequestPayload, IIssuanceCreateOffer } from './interface/agent-service.interface'; +import { GetCredDefAgentRedirection, IAgentSpinupDto, IStoreOrgAgentDetails, ITenantCredDef, ITenantDto, ITenantSchema, IWalletProvision, ISendProofRequestPayload, IIssuanceCreateOffer, OutOfBandCredentialOffer } from './interface/agent-service.interface'; import { AgentType, OrgAgentType } from '@credebl/enum/enum'; import { IConnectionDetails, IUserRequestInterface } from './interface/agent-service.interface'; import { AgentServiceRepository } from './repositories/agent-service.repository'; @@ -133,7 +133,7 @@ export class AgentServiceService { agentSpinupDto.agentType = agentSpinupDto.agentType ? agentSpinupDto.agentType : 1; agentSpinupDto.tenant = agentSpinupDto.tenant ? agentSpinupDto.tenant : false; - agentSpinupDto.ledgerId = !agentSpinupDto.ledgerId || 0 === agentSpinupDto.ledgerId.length ? [3] : agentSpinupDto.ledgerId; + agentSpinupDto.ledgerId = !agentSpinupDto.ledgerId || 0 === agentSpinupDto.ledgerId?.length ? [3] : agentSpinupDto.ledgerId; const platformConfig: platform_config = await this.agentServiceRepository.getPlatformConfigDetails(); @@ -289,7 +289,8 @@ export class AgentServiceService { orgId: orgData.id, walletName: agentSpinupDto.walletName, clientSocketId: agentSpinupDto.clientSocketId, - ledgerId + ledgerId, + did: agentSpinupDto.did }; if (agentEndPoint && agentSpinupDto.clientSocketId) { @@ -338,21 +339,20 @@ export class AgentServiceService { const agentDidWriteUrl = `${payload.agentEndPoint}${CommonConstants.URL_AGENT_WRITE_DID}`; - const { seed, ledgerId } = payload; - const { apiKey } = payload; + const { seed, ledgerId, did, apiKey } = payload; const writeDid = 'write-did'; const ledgerDetails: ledgers[] = await this.agentServiceRepository.getGenesisUrl(ledgerId); - const agentDid = await this._retryAgentSpinup(agentDidWriteUrl, apiKey, writeDid, seed, ledgerDetails[0].indyNamespace); + const agentDid = await this._retryAgentSpinup(agentDidWriteUrl, apiKey, writeDid, seed, ledgerDetails[0].indyNamespace, did); if (agentDid) { - const getDidMethodUrl = `${payload.agentEndPoint}${CommonConstants.URL_AGENT_GET_DIDS}`; + const getDidMethodUrl = `${payload.agentEndPoint}${CommonConstants.URL_AGENT_GET_DID}`.replace('#', agentDid['did']); const getDidDic = 'get-did-doc'; const getDidMethod = await this._retryAgentSpinup(getDidMethodUrl, apiKey, getDidDic); const storeOrgAgentData: IStoreOrgAgentDetails = { - did: getDidMethod[0]?.did, - verkey: getDidMethod[0]?.didDocument?.verificationMethod[0]?.publicKeyBase58, + did: getDidMethod['didDocument']?.id, + verkey: getDidMethod['didDocument']?.verificationMethod[0]?.publicKeyBase58, isDidPublic: true, agentSpinUpStatus: 2, walletName: payload.walletName, @@ -389,14 +389,14 @@ export class AgentServiceService { } } - async _retryAgentSpinup(agentUrl: string, apiKey: string, agentApiState: string, seed?: string, indyNamespace?: string): Promise { + async _retryAgentSpinup(agentUrl: string, apiKey: string, agentApiState: string, seed?: string, indyNamespace?: string, did?: string): Promise { return retry( async () => { if ('write-did' === agentApiState) { const agentDid = await this.commonService - .httpPost(agentUrl, { seed, method: indyNamespace }, { headers: { 'x-api-key': apiKey } }) + .httpPost(agentUrl, { seed, method: indyNamespace, did: did ? did : undefined }, { headers: { 'x-api-key': apiKey } }) .then(async response => response); return agentDid; } else if ('get-did-doc' === agentApiState) { @@ -487,7 +487,7 @@ export class AgentServiceService { async _createTenant(payload: ITenantDto, user: IUserRequestInterface): Promise { try { - payload.ledgerId = !payload.ledgerId || 0 === payload.ledgerId.length ? [3] : payload.ledgerId; + payload.ledgerId = !payload.ledgerId || 0 === payload.ledgerId?.length ? [3] : payload.ledgerId; const ledgerDetails: ledgers[] = await this.agentServiceRepository.getGenesisUrl(payload.ledgerId); const sharedAgentSpinUpResponse = new Promise(async (resolve, _reject) => { @@ -520,12 +520,13 @@ export class AgentServiceService { let tenantDetails; const url = `${platformAdminSpinnedUp.org_agents[0].agentEndPoint}${CommonConstants.URL_SHAGENT_CREATE_TENANT}`; for (const iterator of ledgerDetails) { - const { label, seed } = payload; + const { label, seed, did } = payload; const createTenantOptions = { config: { label }, seed, + did: did ? did : undefined, method: iterator.indyNamespace }; const apiKey = ''; @@ -944,5 +945,16 @@ export class AgentServiceService { } } + async outOfBandCredentialOffer(outOfBandIssuancePayload: OutOfBandCredentialOffer, url: string, apiKey: string): Promise { + try { + const sendOutOfbandCredentialOffer = await this.commonService + .httpPost(url, outOfBandIssuancePayload, { headers: { 'x-api-key': apiKey } }) + .then(async response => response); + return sendOutOfbandCredentialOffer; + } catch (error) { + this.logger.error(`Error in out-of-band credential in agent service : ${JSON.stringify(error)}`); + throw new RpcException(error); + } + } } diff --git a/apps/agent-service/src/interface/agent-service.interface.ts b/apps/agent-service/src/interface/agent-service.interface.ts index 3386889bb..296b8afae 100644 --- a/apps/agent-service/src/interface/agent-service.interface.ts +++ b/apps/agent-service/src/interface/agent-service.interface.ts @@ -7,6 +7,7 @@ export interface IAgentSpinupDto { walletPassword: string; seed: string; orgId: number; + did?: string; ledgerId?: number[]; agentType?: AgentType; transactionApproval?: boolean; @@ -14,12 +15,22 @@ export interface IAgentSpinupDto { tenant?: boolean; } +export interface OutOfBandCredentialOffer { + emailId: string; + attributes: Attributes[]; + credentialDefinitionId: string; + comment: string; + protocolVersion?: string; + orgId: number; +} + export interface ITenantDto { label: string; seed: string; - ledgerId?: number[]; method: string; orgId: number; + did?: string; + ledgerId?: number[]; tenantId?: string; clientSocketId?: string; } @@ -231,10 +242,10 @@ export interface ICredentialFormats { } export interface IIndy { - attributes: IAttributes[]; + attributes: Attributes[]; } -export interface IAttributes { +export interface Attributes { name: string; value: string; } diff --git a/apps/api-gateway/src/agent-service/dto/agent-service.dto.ts b/apps/api-gateway/src/agent-service/dto/agent-service.dto.ts index a4227b193..86415e94d 100644 --- a/apps/api-gateway/src/agent-service/dto/agent-service.dto.ts +++ b/apps/api-gateway/src/agent-service/dto/agent-service.dto.ts @@ -24,7 +24,6 @@ export class AgentSpinupDto { @IsNotEmpty({ message: 'Password is required.' }) walletPassword: string; - @ApiProperty() @Transform(({ value }) => trim(value)) @IsNotEmpty({ message: 'seed is required' }) @@ -35,6 +34,11 @@ export class AgentSpinupDto { }) seed: string; + @ApiProperty() + @IsOptional() + @IsString({ message: 'did must be in string format.' }) + did?: string; + @ApiProperty({ example: [1] }) @IsOptional() @IsArray({ message: 'ledgerId must be an array' }) diff --git a/apps/api-gateway/src/agent-service/dto/create-tenant.dto.ts b/apps/api-gateway/src/agent-service/dto/create-tenant.dto.ts index f0757532a..990969ae9 100644 --- a/apps/api-gateway/src/agent-service/dto/create-tenant.dto.ts +++ b/apps/api-gateway/src/agent-service/dto/create-tenant.dto.ts @@ -33,6 +33,11 @@ export class CreateTenantDto { orgId: number; + @ApiProperty() + @IsOptional() + @IsString({ message: 'did must be in string format.' }) + did?: string; + @ApiProperty() @IsOptional() @ApiPropertyOptional() diff --git a/apps/api-gateway/src/authz/authz.controller.ts b/apps/api-gateway/src/authz/authz.controller.ts index c2100717b..3e35edf4f 100644 --- a/apps/api-gateway/src/authz/authz.controller.ts +++ b/apps/api-gateway/src/authz/authz.controller.ts @@ -1,5 +1,4 @@ import { - BadRequestException, Body, Controller, Get, @@ -83,37 +82,17 @@ export class AuthzController { @Post('/signup') @ApiOperation({ summary: 'Register new user to platform', description: 'Register new user to platform' }) async addUserDetails(@Body() userInfo: AddUserDetails, @Res() res: Response): Promise { - let finalResponse; - let userDetails; - - if (false === userInfo.isPasskey) { - - const decryptedPassword = this.commonService.decryptPassword(userInfo.password); - if (8 <= decryptedPassword.length && 50 >= decryptedPassword.length) { - this.commonService.passwordValidation(decryptedPassword); - userInfo.password = decryptedPassword; - userDetails = await this.authzService.addUserDetails(userInfo); - finalResponse = { - statusCode: HttpStatus.CREATED, - message: ResponseMessages.user.success.create, - data: userDetails.response - }; - } else { - throw new BadRequestException('Password name must be between 8 to 50 Characters'); - } - } else { - - userDetails = await this.authzService.addUserDetails(userInfo); - finalResponse = { + const userDetails = await this.authzService.addUserDetails(userInfo); + const finalResponse = { statusCode: HttpStatus.CREATED, message: ResponseMessages.user.success.create, data: userDetails.response }; - } return res.status(HttpStatus.CREATED).json(finalResponse); } + /** * * @param loginUserDto @@ -130,11 +109,7 @@ export class AuthzController { async login(@Body() loginUserDto: LoginUserDto, @Res() res: Response): Promise { if (loginUserDto.email) { - let decryptedPassword; - if (loginUserDto.password) { - decryptedPassword = this.commonService.decryptPassword(loginUserDto.password); - } - const userData = await this.authzService.login(loginUserDto.email, decryptedPassword, loginUserDto.isPasskey); + const userData = await this.authzService.login(loginUserDto.email, loginUserDto.password, loginUserDto.isPasskey); const finalResponse: IResponseType = { statusCode: HttpStatus.OK, message: ResponseMessages.user.success.login, diff --git a/apps/api-gateway/src/authz/guards/org-roles.guard.ts b/apps/api-gateway/src/authz/guards/org-roles.guard.ts index 46c60aec7..803a7a229 100644 --- a/apps/api-gateway/src/authz/guards/org-roles.guard.ts +++ b/apps/api-gateway/src/authz/guards/org-roles.guard.ts @@ -51,6 +51,21 @@ export class OrgRolesGuard implements CanActivate { } }); + } else if (requiredRolesNames.includes(OrgRoles.PLATFORM_ADMIN)) { + + // eslint-disable-next-line array-callback-return + const isPlatformAdmin = user.userOrgRoles.find((orgDetails) => { + if (orgDetails.orgRole.name === OrgRoles.PLATFORM_ADMIN) { + return true; + } + }); + + if (isPlatformAdmin) { + return true; + } + + return false; + } else { throw new HttpException('organization is required', HttpStatus.BAD_REQUEST); } diff --git a/apps/api-gateway/src/config/multer.config.ts b/apps/api-gateway/src/config/multer.config.ts deleted file mode 100644 index ba46cf06b..000000000 --- a/apps/api-gateway/src/config/multer.config.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { HttpException, HttpStatus } from '@nestjs/common'; -import { diskStorage } from 'multer'; -import * as fs from 'fs'; - - -// Multer upload options -export const multerCSVOptions = { - storage: diskStorage({ - destination: (req, file, cb) => { - const { id } = req.body; - const path = `./uploadedFiles/import`; - fs.mkdirSync(path, { recursive: true }); - return cb(null, path); - }, - filename: (req, file, cb) => { - if ( - 'text/csv' === file.mimetype - ) { - cb(null, `${file.originalname}`); - } else { - cb(new HttpException(`File format should be CSV`, HttpStatus.BAD_REQUEST), ''); - } - } - }) -}; diff --git a/apps/api-gateway/src/connection/connection.controller.ts b/apps/api-gateway/src/connection/connection.controller.ts index d87f58721..1dc42e6ec 100644 --- a/apps/api-gateway/src/connection/connection.controller.ts +++ b/apps/api-gateway/src/connection/connection.controller.ts @@ -129,7 +129,7 @@ export class ConnectionController { @Post('/orgs/:orgId/connections') @ApiOperation({ summary: 'Create outbound out-of-band connection (Legacy Invitation)', description: 'Create outbound out-of-band connection (Legacy Invitation)' }) @UseGuards(AuthGuard('jwt'), OrgRolesGuard) - @Roles(OrgRoles.OWNER, OrgRoles.ADMIN) + @Roles(OrgRoles.OWNER, OrgRoles.ADMIN, OrgRoles.ISSUER, OrgRoles.VERIFIER, OrgRoles.MEMBER) @ApiResponse({ status: 201, description: 'Success', type: AuthTokenResponse }) async createLegacyConnectionInvitation( @Param('orgId') orgId: number, diff --git a/apps/api-gateway/src/credential-definition/credential-definition.controller.ts b/apps/api-gateway/src/credential-definition/credential-definition.controller.ts index cbc1f1f4f..e8603a436 100644 --- a/apps/api-gateway/src/credential-definition/credential-definition.controller.ts +++ b/apps/api-gateway/src/credential-definition/credential-definition.controller.ts @@ -139,4 +139,25 @@ export class CredentialDefinitionController { }; return res.status(HttpStatus.CREATED).json(credDefResponse); } + + @Get('/orgs/:orgId/bulk/cred-defs') + @ApiOperation({ + summary: 'Fetch all credential definition for bulk opeartion', + description: 'Fetch all credential definition from metadata saved in database for bulk opeartion.' + }) + @ApiResponse({ status: 200, description: 'Success', type: ApiResponseDto }) + @Roles(OrgRoles.OWNER, OrgRoles.ADMIN, OrgRoles.ISSUER, OrgRoles.VERIFIER) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + async getAllCredDefAndSchemaForBulkOperation( + @Param('orgId') orgId: number, + @Res() res: Response + ): Promise { + const credentialsDefinitionDetails = await this.credentialDefinitionService.getAllCredDefAndSchemaForBulkOperation(orgId); + const credDefResponse: IResponseType = { + statusCode: HttpStatus.OK, + message: ResponseMessages.credentialDefinition.success.fetch, + data: credentialsDefinitionDetails.response + }; + return res.status(HttpStatus.CREATED).json(credDefResponse); + } } \ No newline at end of file diff --git a/apps/api-gateway/src/credential-definition/credential-definition.service.ts b/apps/api-gateway/src/credential-definition/credential-definition.service.ts index 799bbed7f..11d1a1feb 100644 --- a/apps/api-gateway/src/credential-definition/credential-definition.service.ts +++ b/apps/api-gateway/src/credential-definition/credential-definition.service.ts @@ -33,4 +33,9 @@ export class CredentialDefinitionService extends BaseService { const payload = { schemaId }; return this.sendNats(this.credDefServiceProxy, 'get-all-credential-definitions-by-schema-id', payload); } + + getAllCredDefAndSchemaForBulkOperation(orgId:number): Promise<{ response: object }> { + const payload = { orgId }; + return this.sendNats(this.credDefServiceProxy, 'get-all-schema-cred-defs-for-bulk-operation', payload); + } } diff --git a/apps/api-gateway/src/ecosystem/dtos/create-ecosystem-dto.ts b/apps/api-gateway/src/ecosystem/dtos/create-ecosystem-dto.ts index 1dc36955c..8095ccd53 100644 --- a/apps/api-gateway/src/ecosystem/dtos/create-ecosystem-dto.ts +++ b/apps/api-gateway/src/ecosystem/dtos/create-ecosystem-dto.ts @@ -1,5 +1,5 @@ import { ApiExtraModels, ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { IsInt, IsNotEmpty, IsOptional, IsString, MaxLength, MinLength } from 'class-validator'; +import { IsBoolean, IsInt, IsNotEmpty, IsOptional, IsString, MaxLength, MinLength } from 'class-validator'; import { Transform } from 'class-transformer'; import { trim } from '@credebl/common/cast.helper'; @@ -53,6 +53,12 @@ export class CreateEcosystemDto { @IsString({ message: 'organization did must be in string format.' }) orgDid: string; + @ApiPropertyOptional({ example: 'false' }) + @IsBoolean() + @IsOptional() + @IsNotEmpty({ message: 'autoEndorsement should be boolean value' }) + autoEndorsement = false; + orgId?: string; } \ No newline at end of file diff --git a/apps/api-gateway/src/ecosystem/dtos/edit-ecosystem-dto.ts b/apps/api-gateway/src/ecosystem/dtos/edit-ecosystem-dto.ts index 96be3ba09..917761bd6 100644 --- a/apps/api-gateway/src/ecosystem/dtos/edit-ecosystem-dto.ts +++ b/apps/api-gateway/src/ecosystem/dtos/edit-ecosystem-dto.ts @@ -1,5 +1,5 @@ import { ApiExtraModels, ApiPropertyOptional } from '@nestjs/swagger'; -import { IsNotEmpty, IsOptional, IsString, MaxLength, MinLength } from 'class-validator'; +import { IsBoolean, IsNotEmpty, IsOptional, IsString, MaxLength, MinLength } from 'class-validator'; import { Transform } from 'class-transformer'; import { trim } from '@credebl/common/cast.helper'; @@ -34,5 +34,11 @@ export class EditEcosystemDto { @Transform(({ value }) => trim(value)) @IsString({ message: 'logo must be in string format.' }) logo?: string; + + @ApiPropertyOptional({ example: 'false' }) + @IsBoolean() + @IsOptional() + @IsNotEmpty({ message: 'autoEndorsement should be boolean value' }) + autoEndorsement = false; } \ No newline at end of file diff --git a/apps/api-gateway/src/ecosystem/ecosystem.controller.ts b/apps/api-gateway/src/ecosystem/ecosystem.controller.ts index f8212a7fe..89f986e5c 100644 --- a/apps/api-gateway/src/ecosystem/ecosystem.controller.ts +++ b/apps/api-gateway/src/ecosystem/ecosystem.controller.ts @@ -80,6 +80,44 @@ export class EcosystemController { return res.status(HttpStatus.OK).json(finalResponse); } + @Get('/:ecosystemId/:orgId/schemas') + @ApiOperation({ summary: 'Get all ecosystem schemas', description: 'Get all ecosystem schemas' }) + @ApiResponse({ status: 200, description: 'Success', type: ApiResponseDto }) + @UseGuards(AuthGuard('jwt'), EcosystemRolesGuard, OrgRolesGuard) + @ApiBearerAuth() + @EcosystemsRoles(EcosystemRoles.ECOSYSTEM_OWNER, EcosystemRoles.ECOSYSTEM_LEAD, EcosystemRoles.ECOSYSTEM_MEMBER) + @Roles(OrgRoles.OWNER, OrgRoles.ADMIN, OrgRoles.ISSUER, OrgRoles.VERIFIER, OrgRoles.MEMBER) + @ApiQuery({ + name: 'pageNumber', + type: Number, + required: false + }) + @ApiQuery({ + name: 'search', + type: String, + required: false + }) + @ApiQuery({ + name: 'pageSize', + type: Number, + required: false + }) + async getAllEcosystemSchemas( + @Param('ecosystemId') ecosystemId: string, + @Param('orgId') orgId: string, + @Query() getAllEcosystemSchemaDto: GetAllEcosystemInvitationsDto, + @Res() res: Response + ): Promise { + + const schemaList = await this.ecosystemService.getAllEcosystemSchemas(ecosystemId, orgId, getAllEcosystemSchemaDto); + const finalResponse: IResponseType = { + statusCode: HttpStatus.OK, + message: ResponseMessages.ecosystem.success.allschema, + data: schemaList.response + }; + return res.status(HttpStatus.OK).json(finalResponse); + } + @Get('/:orgId') @ApiOperation({ summary: 'Get all organization ecosystems', description: 'Get all existing ecosystems of an specific organization' }) @ApiResponse({ status: 200, description: 'Success', type: ApiResponseDto }) diff --git a/apps/api-gateway/src/ecosystem/ecosystem.service.ts b/apps/api-gateway/src/ecosystem/ecosystem.service.ts index 3a7e6b2e7..11db73d99 100644 --- a/apps/api-gateway/src/ecosystem/ecosystem.service.ts +++ b/apps/api-gateway/src/ecosystem/ecosystem.service.ts @@ -142,6 +142,16 @@ export class EcosystemService extends BaseService { return this.sendNats(this.serviceProxy, 'get-endorsement-transactions', payload); } + async getAllEcosystemSchemas( + ecosystemId: string, + orgId: string, + getAllEcosystemSchemaDto: GetAllEcosystemInvitationsDto + ): Promise<{ response: object }> { + const { pageNumber, pageSize, search } = getAllEcosystemSchemaDto; + const payload = { ecosystemId, orgId, pageNumber, pageSize, search }; + return this.sendNats(this.serviceProxy, 'get-all-ecosystem-schemas', payload); + } + async schemaEndorsementRequest(requestSchemaPayload: RequestSchemaDto, orgId: number, ecosystemId: string): Promise { const payload = { requestSchemaPayload, orgId, ecosystemId }; diff --git a/apps/api-gateway/src/enum.ts b/apps/api-gateway/src/enum.ts index 7dd84939b..d3f8d2110 100644 --- a/apps/api-gateway/src/enum.ts +++ b/apps/api-gateway/src/enum.ts @@ -116,3 +116,13 @@ export enum ExpiredSubscriptionSortBy { endDate = 'endDate', id = 'id', } + +export enum FileUploadType { + Issuance = 'ISSUANCE' +} + +export enum FileUploadStatus { + started = 'PROCESS_STARTED', + completed = 'PROCESS_COMPLETED', + interrupted= 'PROCESS INTERRUPTED' +} diff --git a/apps/api-gateway/src/helper-files/file-operation.helper.ts b/apps/api-gateway/src/helper-files/file-operation.helper.ts new file mode 100644 index 000000000..e02d7315b --- /dev/null +++ b/apps/api-gateway/src/helper-files/file-operation.helper.ts @@ -0,0 +1,36 @@ +import { promisify } from "util"; +import * as fs from "fs"; + + +export const createFile = async ( + path: string, + fileName: string, + data: string + ): Promise => { + // eslint-disable-next-line @typescript-eslint/no-use-before-define + if (!checkIfFileOrDirectoryExists(path)) { + + fs.mkdirSync(path); + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const writeFile = promisify(fs.writeFile); + return fs.writeFileSync(`${path}/${fileName}`, data, 'utf8'); + }; + + export const checkIfFileOrDirectoryExists = (path: string): boolean => fs.existsSync(path); + + export const getFile = async ( + path: string, + encoding: BufferEncoding + ): Promise => { + const readFile = promisify(fs.readFile); + + return encoding ? readFile(path, {encoding}) : readFile(path, {}); + }; + + + export const deleteFile = async (path: string): Promise => { + const unlink = promisify(fs.unlink); + + return unlink(path); + }; \ No newline at end of file diff --git a/apps/api-gateway/src/issuance/dtos/issuance.dto.ts b/apps/api-gateway/src/issuance/dtos/issuance.dto.ts index e2f6a4c37..148f65f57 100644 --- a/apps/api-gateway/src/issuance/dtos/issuance.dto.ts +++ b/apps/api-gateway/src/issuance/dtos/issuance.dto.ts @@ -1,7 +1,14 @@ -import { IsArray, IsNotEmpty, IsOptional, IsString } from 'class-validator'; +import { IsArray, IsNotEmpty, IsOptional, IsString, IsEmail } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; +import { Transform, Type } from 'class-transformer'; +import { toNumber } from '@credebl/common/cast.helper'; -interface attribute { +interface CredentialOffer { + emailId: string; + attributes: Attribute[]; +} + +interface Attribute { name: string; value: string; } @@ -11,7 +18,7 @@ export class IssueCredentialDto { @ApiProperty({ example: [{ 'value': 'string', 'name': 'string' }] }) @IsNotEmpty({ message: 'Please provide valid attributes' }) @IsArray({ message: 'attributes should be array' }) - attributes: attribute[]; + attributes: Attribute[]; @ApiProperty({ example: 'string' }) @IsNotEmpty({ message: 'Please provide valid credentialDefinitionId' }) @@ -98,3 +105,74 @@ export class CredentialAttributes { value: string; } +export class OutOfBandCredentialDto { + + @ApiProperty({ example: [{ 'emailId': 'abc@example.com', 'attribute': [{ 'value': 'string', 'name': 'string' }] }] }) + @IsNotEmpty({ message: 'Please provide valid attributes' }) + @IsArray({ message: 'attributes should be array' }) + @IsOptional() + credentialOffer: CredentialOffer[]; + + @ApiProperty({ example: 'awqx@getnada.com' }) + @IsEmail() + @IsNotEmpty({ message: 'Please provide valid email' }) + @IsString({ message: 'email should be string' }) + @IsOptional() + emailId: string; + + @ApiProperty({ example: [{ 'value': 'string', 'name': 'string' }] }) + @IsNotEmpty({ message: 'Please provide valid attributes' }) + @IsArray({ message: 'attributes should be array' }) + @IsOptional() + attributes: Attribute[]; + + @ApiProperty({ example: 'string' }) + @IsNotEmpty({ message: 'Please provide valid credential definition id' }) + @IsString({ message: 'credential definition id should be string' }) + credentialDefinitionId: string; + + @ApiProperty({ example: 'string' }) + @IsNotEmpty({ message: 'Please provide valid comment' }) + @IsString({ message: 'comment should be string' }) + @IsOptional() + comment: string; + + @ApiProperty({ example: 'v1' }) + @IsOptional() + @IsNotEmpty({ message: 'Please provide valid protocol version' }) + @IsString({ message: 'protocol version should be string' }) + protocolVersion?: string; + + orgId: number; +} + + +export class PreviewFileDetails { + @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 46ff1044a..b6c8b0307 100644 --- a/apps/api-gateway/src/issuance/interfaces/index.ts +++ b/apps/api-gateway/src/issuance/interfaces/index.ts @@ -54,4 +54,16 @@ export class IUserRole { export class IUserOrg { id: number; orgName: string; -} \ No newline at end of file +} + +export interface FileExportResponse { + response: unknown; + fileContent: string; + fileName : string +} + +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 ba130ad24..75259fa0d 100644 --- a/apps/api-gateway/src/issuance/issuance.controller.ts +++ b/apps/api-gateway/src/issuance/issuance.controller.ts @@ -33,7 +33,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 } from './dtos/issuance.dto'; +import { IssuanceDto, IssueCredentialDto, OutOfBandCredentialDto } 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'; @@ -42,7 +42,7 @@ import { Roles } from '../authz/decorators/roles.decorator'; import { OrgRoles } from 'libs/org-roles/enums'; import { OrgRolesGuard } from '../authz/guards/org-roles.guard'; import { CustomExceptionFilter } from 'apps/api-gateway/common/exception-handler'; - +import { ImageServiceService } from '@credebl/image-service'; @Controller() @UseFilters(CustomExceptionFilter) @ApiTags('credentials') @@ -52,10 +52,12 @@ import { CustomExceptionFilter } from 'apps/api-gateway/common/exception-handler export class IssuanceController { constructor( private readonly issueCredentialService: IssuanceService, + private readonly imageServiceService: ImageServiceService, private readonly commonService: CommonService ) { } private readonly logger = new Logger('IssuanceController'); + private readonly PAGE: number = 1; /** * Description: Get all issued credentials @@ -183,6 +185,42 @@ export class IssuanceController { return res.status(HttpStatus.CREATED).json(finalResponse); } + /** + * Description: credential issuance out-of-band + * @param user + * @param outOfBandCredentialDto + * @param orgId + * @param res + * @returns + */ + @Post('/orgs/:orgId/credentials/oob') + @UseGuards(AuthGuard('jwt')) + @ApiOperation({ + summary: `Create out-of-band credential offer`, + description: `Create out-of-band credential offer` + }) + @ApiResponse({ status: 201, description: 'Success', type: ApiResponseDto }) + @ApiBearerAuth() + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + @Roles(OrgRoles.OWNER, OrgRoles.ADMIN, OrgRoles.ISSUER) + async outOfBandCredentialOffer( + @User() user: IUserRequest, + @Body() outOfBandCredentialDto: OutOfBandCredentialDto, + @Param('orgId') orgId: number, + @Res() res: Response + ): Promise { + + outOfBandCredentialDto.orgId = orgId; + const getCredentialDetails = await this.issueCredentialService.outOfBandCredentialOffer(user, outOfBandCredentialDto); + + const finalResponse: IResponseType = { + statusCode: HttpStatus.CREATED, + message: ResponseMessages.issuance.success.fetch, + data: getCredentialDetails.response + }; + return res.status(HttpStatus.CREATED).json(finalResponse); + } + /** * Description: webhook Save issued credential details * @param user diff --git a/apps/api-gateway/src/issuance/issuance.module.ts b/apps/api-gateway/src/issuance/issuance.module.ts index 5e339bd5b..bde5e1d4d 100644 --- a/apps/api-gateway/src/issuance/issuance.module.ts +++ b/apps/api-gateway/src/issuance/issuance.module.ts @@ -4,6 +4,7 @@ import { IssuanceController } from './issuance.controller'; import { IssuanceService } from './issuance.service'; import { CommonService } from '@credebl/common'; import { HttpModule } from '@nestjs/axios'; +import { ImageServiceService } from '@credebl/image-service'; @Module({ imports: [ @@ -19,6 +20,6 @@ import { HttpModule } from '@nestjs/axios'; ]) ], controllers: [IssuanceController], - providers: [IssuanceService, CommonService] + providers: [IssuanceService, ImageServiceService, CommonService] }) export class IssuanceModule { } diff --git a/apps/api-gateway/src/issuance/issuance.service.ts b/apps/api-gateway/src/issuance/issuance.service.ts index 38bb28cff..84b87cfaf 100644 --- a/apps/api-gateway/src/issuance/issuance.service.ts +++ b/apps/api-gateway/src/issuance/issuance.service.ts @@ -1,8 +1,9 @@ +/* eslint-disable camelcase */ 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 } from './dtos/issuance.dto'; +import { IssuanceDto, IssueCredentialDto, OutOfBandCredentialDto } from './dtos/issuance.dto'; @Injectable() export class IssuanceService extends BaseService { @@ -50,4 +51,11 @@ export class IssuanceService extends BaseService { return this.sendNats(this.issuanceProxy, 'webhook-get-issue-credential', payload); } + outOfBandCredentialOffer(user: IUserRequest, outOfBandCredentialDto: OutOfBandCredentialDto): Promise<{ + response: object; + }> { + const payload = { user, outOfBandCredentialDto }; + return this.sendNats(this.issuanceProxy, 'out-of-band-credential-offer', payload); + } + } diff --git a/apps/api-gateway/src/main.ts b/apps/api-gateway/src/main.ts index 828a21b51..a9ec07d56 100644 --- a/apps/api-gateway/src/main.ts +++ b/apps/api-gateway/src/main.ts @@ -7,33 +7,18 @@ import { Logger, ValidationPipe } from '@nestjs/common'; import { AppModule } from './app.module'; import { HttpAdapterHost, NestFactory } from '@nestjs/core'; import { AllExceptionsFilter } from '@credebl/common/exception-handler'; - -// const fs = require('fs'); - - +import helmet from "helmet"; dotenv.config(); -// async function readSecretFile(filename: string): Promise { -// return fs.readFile(filename, 'utf8', function (err, data) { -// // Display the file content -// return data; -// }); -// } - async function bootstrap(): Promise { - - // const httpsOptions = { - // key: await readSecretFile(''), - // cert: await readSecretFile(''), - // }; - - // const config = new ConfigService(); const app = await NestFactory.create(AppModule, { // httpsOptions, }); + const expressApp = app.getHttpAdapter().getInstance(); + expressApp.set('x-powered-by', false); app.use(express.json({ limit: '50mb' })); app.use(express.urlencoded({ limit: '50mb' })); - + app.use(helmet()); app.useGlobalPipes(new ValidationPipe()); const options = new DocumentBuilder() .setTitle(`${process.env.PLATFORM_NAME}`) @@ -58,12 +43,10 @@ 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('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('uploadedFiles/exports')); 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/api-gateway/src/organization/organization.controller.ts b/apps/api-gateway/src/organization/organization.controller.ts index 9ac60d335..3746ca933 100644 --- a/apps/api-gateway/src/organization/organization.controller.ts +++ b/apps/api-gateway/src/organization/organization.controller.ts @@ -28,6 +28,7 @@ import { UpdateOrganizationDto } from './dtos/update-organization-dto'; import { CustomExceptionFilter } from 'apps/api-gateway/common/exception-handler'; import { IUserRequestInterface } from '../interfaces/IUserRequestInterface'; import { GetAllUsersDto } from '../user/dto/get-all-users.dto'; +import { ImageServiceService } from '@credebl/image-service'; @UseFilters(CustomExceptionFilter) @Controller('orgs') @@ -38,6 +39,7 @@ export class OrganizationController { constructor( private readonly organizationService: OrganizationService, + private readonly imageServiceService: ImageServiceService, private readonly commonService: CommonService ) { } @@ -47,11 +49,11 @@ export class OrganizationController { async getOgPofile(@Param('orgId') orgId: number, @Res() res: Response): Promise { const orgProfile = await this.organizationService.getOgPofile(orgId); - const base64Data = orgProfile.response["logoUrl"].replace(/^data:image\/\w+;base64,/, ''); + const base64Data = orgProfile.response["logoUrl"]; + const getImageBuffer = await this.imageServiceService.getBase64Image(base64Data); - const imageBuffer = Buffer.from(base64Data, 'base64'); res.setHeader('Content-Type', 'image/png'); - return res.send(imageBuffer); + return res.send(getImageBuffer); } /** diff --git a/apps/api-gateway/src/organization/organization.module.ts b/apps/api-gateway/src/organization/organization.module.ts index 9357db1bd..bb4cc3b58 100644 --- a/apps/api-gateway/src/organization/organization.module.ts +++ b/apps/api-gateway/src/organization/organization.module.ts @@ -6,6 +6,7 @@ import { HttpModule } from '@nestjs/axios'; import { Module } from '@nestjs/common'; import { OrganizationController } from './organization.controller'; import { OrganizationService } from './organization.service'; +import { ImageServiceService } from '@credebl/image-service'; @Module({ imports: [ @@ -23,7 +24,7 @@ import { OrganizationService } from './organization.service'; ]) ], controllers: [OrganizationController], - providers: [OrganizationService, CommonService] + providers: [OrganizationService, CommonService, ImageServiceService] }) export class OrganizationModule { } diff --git a/apps/api-gateway/src/user/dto/update-platform-settings.dto.ts b/apps/api-gateway/src/user/dto/update-platform-settings.dto.ts new file mode 100644 index 000000000..ee63dd8c0 --- /dev/null +++ b/apps/api-gateway/src/user/dto/update-platform-settings.dto.ts @@ -0,0 +1,43 @@ +import { ApiProperty } from '@nestjs/swagger'; +import {IsBoolean, IsNotEmpty, IsOptional, IsString } from 'class-validator'; + + +export class UpdatePlatformSettingsDto { + @ApiProperty({ example: '127.0.0.1' }) + @IsOptional() + @IsString({ message: 'external Ip should be string' }) + externalIp: string; + + @ApiProperty({ example: '127.0.0.1' }) + @IsOptional() + @IsString({ message: 'last Internal Id should be string' }) + lastInternalId: string; + + @ApiProperty() + @IsOptional() + @IsString({ message: 'sgApiKey should be string' }) + sgApiKey: string; + + @ApiProperty({ example: 'abc13@yopmail.com' }) + @IsOptional() + @IsString({ message: 'emailFrom should be string' }) + emailFrom: string; + + @ApiProperty({ example: 'dev.credebl.id' }) + @IsOptional() + @IsString({ message: 'API endpoint should be string' }) + apiEndPoint: string; + + @ApiProperty({ example: 'true' }) + @IsBoolean() + @IsOptional() + @IsNotEmpty({ message: 'enableEcosystem should boolean' }) + enableEcosystem: boolean; + + @ApiProperty({ example: 'true' }) + @IsBoolean() + @IsOptional() + @IsNotEmpty({ message: 'multiEcosystemSupport should boolean' }) + multiEcosystemSupport: boolean; + +} \ No newline at end of file diff --git a/apps/api-gateway/src/user/user.controller.ts b/apps/api-gateway/src/user/user.controller.ts index b14d9e3c6..df5476a9b 100644 --- a/apps/api-gateway/src/user/user.controller.ts +++ b/apps/api-gateway/src/user/user.controller.ts @@ -29,6 +29,10 @@ import { UpdateUserProfileDto } from './dto/update-user-profile.dto'; import { CustomExceptionFilter } from 'apps/api-gateway/common/exception-handler'; import { AddPasskeyDetails } from './dto/add-user.dto'; import { EmailValidator } from '../dtos/email-validator.dto'; +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'; @UseFilters(CustomExceptionFilter) @Controller('users') @@ -119,6 +123,24 @@ export class UserController { } + @Get('/platform-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() + async getPlatformSettings(@Res() res: Response): Promise { + const settings = await this.userService.getPlatformSettings(); + + const finalResponse = { + statusCode: HttpStatus.OK, + message: ResponseMessages.user.success.fetchPlatformSettings, + data: settings.response + }; + + return res.status(HttpStatus.OK).json(finalResponse); + + } + @Get('/activity') @ApiOperation({ summary: 'organization invitations', @@ -272,4 +294,21 @@ export class UserController { } + @Put('/platform-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 { + const result = await this.userService.updatePlatformSettings(platformSettings); + + const finalResponse = { + statusCode: HttpStatus.OK, + message: result.response + }; + + return res.status(HttpStatus.OK).json(finalResponse); + + } + } \ No newline at end of file diff --git a/apps/api-gateway/src/user/user.service.ts b/apps/api-gateway/src/user/user.service.ts index 63e8ecc9e..4409f0f8f 100644 --- a/apps/api-gateway/src/user/user.service.ts +++ b/apps/api-gateway/src/user/user.service.ts @@ -7,6 +7,7 @@ import { GetAllInvitationsDto } from './dto/get-all-invitations.dto'; 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'; @Injectable() export class UserService extends BaseService { @@ -71,4 +72,13 @@ export class UserService extends BaseService { const payload = { userEmail, userInfo }; return this.sendNats(this.serviceProxy, 'add-passkey', payload); } + + async updatePlatformSettings(platformSettings: UpdatePlatformSettingsDto): Promise<{ response: string }> { + const payload = { platformSettings }; + return this.sendNats(this.serviceProxy, 'update-platform-settings', payload); + } + + async getPlatformSettings(): Promise<{ response: object }> { + return this.sendNats(this.serviceProxy, 'fetch-platform-settings', ''); + } } diff --git a/apps/api-gateway/src/verification/dto/request-proof.dto.ts b/apps/api-gateway/src/verification/dto/request-proof.dto.ts index 7081a3f58..c991ba434 100644 --- a/apps/api-gateway/src/verification/dto/request-proof.dto.ts +++ b/apps/api-gateway/src/verification/dto/request-proof.dto.ts @@ -1,4 +1,4 @@ -import { IsArray, IsEmail, IsNotEmpty, IsObject, IsOptional, IsString, MaxLength } from 'class-validator'; +import { IsArray, IsNotEmpty, IsObject, IsOptional, IsString, MaxLength } from 'class-validator'; import { toLowerCase, trim } from '@credebl/common/cast.helper'; import { ApiProperty } from '@nestjs/swagger'; import { Transform } from 'class-transformer'; @@ -85,15 +85,10 @@ export class OutOfBandRequestProof { @IsNotEmpty({ message: 'please provide valid attributes' }) attributes: ProofRequestAttribute[]; - @ApiProperty({ example: 'string' }) - @IsNotEmpty({ message: 'Please provide valid emailId' }) - @Transform(({ value }) => trim(value)) - @Transform(({ value }) => toLowerCase(value)) - @IsNotEmpty({ message: 'Email is required.' }) - @MaxLength(256, { message: 'Email must be at most 256 character.' }) - @IsEmail() - emailId: string; - + @ApiProperty() + @IsString({ each: true, message: 'Each emailId in the array should be a string' }) + emailId: string | string[]; + @ApiProperty() @IsOptional() comment: string; diff --git a/apps/api-gateway/src/verification/verification.controller.ts b/apps/api-gateway/src/verification/verification.controller.ts index f9f555c52..2bc992364 100644 --- a/apps/api-gateway/src/verification/verification.controller.ts +++ b/apps/api-gateway/src/verification/verification.controller.ts @@ -29,13 +29,16 @@ import { AuthGuard } from '@nestjs/passport'; import { OrgRolesGuard } from '../authz/guards/org-roles.guard'; import { WebhookPresentationProof } from './dto/webhook-proof.dto'; import { CustomExceptionFilter } from 'apps/api-gateway/common/exception-handler'; +import { ImageServiceService } from '@credebl/image-service'; @UseFilters(CustomExceptionFilter) -@ApiBearerAuth() @Controller() @ApiTags('verifications') export class VerificationController { - constructor(private readonly verificationService: VerificationService) { } + constructor( + private readonly verificationService: VerificationService, + private readonly imageServiceService: ImageServiceService + ) { } private readonly logger = new Logger('VerificationController'); @@ -46,6 +49,7 @@ export class VerificationController { }) @Roles(OrgRoles.OWNER, OrgRoles.ADMIN, OrgRoles.ISSUER, OrgRoles.VERIFIER, OrgRoles.MEMBER, OrgRoles.HOLDER) @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 }) @@ -81,6 +85,7 @@ export class VerificationController { @ApiForbiddenResponse({ status: 403, description: 'Forbidden', type: ForbiddenErrorDto }) @Roles(OrgRoles.OWNER, OrgRoles.ADMIN, OrgRoles.ISSUER, OrgRoles.VERIFIER, OrgRoles.MEMBER, OrgRoles.HOLDER) @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + @ApiBearerAuth() async getProofPresentationById( @Res() res: Response, @GetUser() user: IUserRequest, @@ -113,6 +118,7 @@ export class VerificationController { @ApiQuery( { name: 'threadId', required: false } ) + @ApiBearerAuth() @Roles(OrgRoles.OWNER, OrgRoles.ADMIN, OrgRoles.ISSUER, OrgRoles.VERIFIER, OrgRoles.MEMBER, OrgRoles.HOLDER) @UseGuards(AuthGuard('jwt'), OrgRolesGuard) async getProofPresentations( @@ -145,6 +151,7 @@ export class VerificationController { @ApiUnauthorizedResponse({ status: 401, description: 'Unauthorized', type: UnauthorizedErrorDto }) @ApiForbiddenResponse({ status: 403, description: 'Forbidden', type: ForbiddenErrorDto }) @ApiBody({ type: RequestProof }) + @ApiBearerAuth() @UseGuards(AuthGuard('jwt'), OrgRolesGuard) @Roles(OrgRoles.OWNER, OrgRoles.ADMIN, OrgRoles.VERIFIER) async sendPresentationRequest( @@ -184,6 +191,7 @@ export class VerificationController { @ApiUnauthorizedResponse({ status: 401, description: 'Unauthorized', type: UnauthorizedErrorDto }) @ApiForbiddenResponse({ status: 403, description: 'Forbidden', type: ForbiddenErrorDto }) @Roles(OrgRoles.OWNER, OrgRoles.ADMIN, OrgRoles.VERIFIER) + @ApiBearerAuth() @UseGuards(AuthGuard('jwt'), OrgRolesGuard) async verifyPresentation( @Res() res: Response, @@ -216,6 +224,7 @@ export class VerificationController { @ApiForbiddenResponse({ status: 403, description: 'Forbidden', type: ForbiddenErrorDto }) @ApiBody({ type: OutOfBandRequestProof }) @Roles(OrgRoles.OWNER, OrgRoles.ADMIN, OrgRoles.VERIFIER) + @ApiBearerAuth() @UseGuards(AuthGuard('jwt'), OrgRolesGuard) async sendOutOfBandPresentationRequest( @Res() res: Response, diff --git a/apps/api-gateway/src/verification/verification.module.ts b/apps/api-gateway/src/verification/verification.module.ts index 0c71b71bf..ee8e7bcab 100644 --- a/apps/api-gateway/src/verification/verification.module.ts +++ b/apps/api-gateway/src/verification/verification.module.ts @@ -4,6 +4,7 @@ import { ConfigModule } from '@nestjs/config'; import { Module } from '@nestjs/common'; import { VerificationController } from './verification.controller'; import { VerificationService } from './verification.service'; +import { ImageServiceService } from '@credebl/image-service'; @Module({ imports: [ @@ -19,6 +20,6 @@ import { VerificationService } from './verification.service'; ]) ], controllers: [VerificationController], - providers: [VerificationService] + providers: [VerificationService, ImageServiceService] }) export class VerificationModule { } diff --git a/apps/connection/src/connection.repository.ts b/apps/connection/src/connection.repository.ts index 014510354..4623bedac 100644 --- a/apps/connection/src/connection.repository.ts +++ b/apps/connection/src/connection.repository.ts @@ -1,7 +1,8 @@ import { Injectable, InternalServerErrorException, Logger } from '@nestjs/common'; import { PrismaService } from '@credebl/prisma-service'; // eslint-disable-next-line camelcase -import { agent_invitations, connections, organisation, platform_config, shortening_url } from '@prisma/client'; +import { agent_invitations, connections, platform_config, shortening_url } from '@prisma/client'; +import { OrgAgent } from './interfaces/connection.interfaces'; @Injectable() export class ConnectionRepository { @@ -16,23 +17,7 @@ export class ConnectionRepository { * @returns Get getAgentEndPoint details */ // eslint-disable-next-line camelcase - async getAgentEndPoint(orgId: number): Promise<{ - organisation: organisation; - } & { - id: number; - createDateTime: Date; - createdBy: number; - lastChangedDateTime: Date; - lastChangedBy: number; - orgDid: string; - verkey: string; - agentEndPoint: string; - agentId: number; - isDidPublic: boolean; - ledgerId: number; - orgAgentTypeId: number; - tenantId: string; - }> { + async getAgentEndPoint(orgId: number): Promise { try { const agentDetails = await this.prisma.org_agents.findFirst({ @@ -78,6 +63,28 @@ export class ConnectionRepository { } } + /** + * Get agent invitation by orgId + * @param orgId + * @returns Get connection details + */ + // eslint-disable-next-line camelcase + async getConnectionInvitationByOrgId(orgId: number): Promise { + try { + + const agentInvitationDetails = await this.prisma.agent_invitations.findFirst({ + where: { + orgId + } + }); + return agentInvitationDetails; + + } catch (error) { + this.logger.error(`Error in saveAgentConnectionInvitations: ${error.message} `); + throw error; + } + } + /** * Description: Save connection details * @param connectionInvitation diff --git a/apps/connection/src/connection.service.ts b/apps/connection/src/connection.service.ts index 502f002bd..ff3dbebe8 100644 --- a/apps/connection/src/connection.service.ts +++ b/apps/connection/src/connection.service.ts @@ -44,6 +44,13 @@ export class ConnectionService { orgId: number, user: IUserRequestInterface, multiUseInvitation: boolean, autoAcceptConnection: boolean, alias: string, imageUrl: string, label: string ): Promise { try { + + const connectionInvitationExist = await this.connectionRepository.getConnectionInvitationByOrgId(orgId); + + if (connectionInvitationExist) { + return connectionInvitationExist; + } + const agentDetails = await this.connectionRepository.getAgentEndPoint(orgId); const platformConfig: platform_config = await this.connectionRepository.getPlatformConfigDetails(); const { agentEndPoint, id, organisation } = agentDetails; @@ -57,7 +64,6 @@ export class ConnectionService { logoImageUrl = `${process.env.API_GATEWAY_PROTOCOL}://${process.env.API_ENDPOINT}/orgs/profile/${organisation.id}`; } - this.logger.log(`logoImageUrl ::: ${logoImageUrl}`); const connectionPayload = { multiUseInvitation: multiUseInvitation || true, autoAcceptConnection: autoAcceptConnection || true, diff --git a/apps/connection/src/interfaces/connection.interfaces.ts b/apps/connection/src/interfaces/connection.interfaces.ts index dc078ea4d..4aed524a8 100644 --- a/apps/connection/src/interfaces/connection.interfaces.ts +++ b/apps/connection/src/interfaces/connection.interfaces.ts @@ -1,5 +1,6 @@ // eslint-disable-next-line camelcase import { IUserRequest } from '@credebl/user-request/user-request.interface'; +import { organisation } from '@prisma/client'; import { UserRoleOrgPermsDto } from 'apps/api-gateway/src/dtos/user-role-org-perms.dto'; export interface IConnection { @@ -91,4 +92,21 @@ export interface ConnectionInvitationResponse { message: { invitation: object; }; -} \ No newline at end of file +} + +export interface OrgAgent { + organisation: organisation; + id: number; + createDateTime: Date; + createdBy: number; + lastChangedDateTime: Date; + lastChangedBy: number; + orgDid: string; + verkey: string; + agentEndPoint: string; + agentId: number; + isDidPublic: boolean; + ledgerId: number; + orgAgentTypeId: number; + tenantId: string; +} diff --git a/apps/ecosystem/interfaces/ecosystem.interfaces.ts b/apps/ecosystem/interfaces/ecosystem.interfaces.ts index e77392db7..4b0208733 100644 --- a/apps/ecosystem/interfaces/ecosystem.interfaces.ts +++ b/apps/ecosystem/interfaces/ecosystem.interfaces.ts @@ -189,4 +189,6 @@ export interface CreateEcosystem { orgDid: string; orgId?: string; + + autoEndorsement: boolean } \ No newline at end of file diff --git a/apps/ecosystem/interfaces/endorsements.interface.ts b/apps/ecosystem/interfaces/endorsements.interface.ts index d4c837c06..a8024a05e 100644 --- a/apps/ecosystem/interfaces/endorsements.interface.ts +++ b/apps/ecosystem/interfaces/endorsements.interface.ts @@ -6,4 +6,13 @@ export interface GetEndorsementsPayload { pageSize: number; search: string; type: string; + } + + export interface GetAllSchemaList { + ecosystemId: string; + orgId: string; + status: string; + pageNumber: number; + pageSize: number; + search: string; } \ No newline at end of file diff --git a/apps/ecosystem/src/ecosystem.controller.ts b/apps/ecosystem/src/ecosystem.controller.ts index e996dc550..46f27815d 100644 --- a/apps/ecosystem/src/ecosystem.controller.ts +++ b/apps/ecosystem/src/ecosystem.controller.ts @@ -133,6 +133,16 @@ export class EcosystemController { ); } + + @MessagePattern({ cmd: 'get-all-ecosystem-schemas' }) + async getAllEcosystemSchemas( + @Body() payload: GetEndorsementsPayload + ): Promise { + return this.ecosystemService.getAllEcosystemSchemas( + payload + ); + } + @MessagePattern({ cmd: 'delete-ecosystem-invitations' }) async deleteInvitation( @Body() payload: { invitationId: string } diff --git a/apps/ecosystem/src/ecosystem.repository.ts b/apps/ecosystem/src/ecosystem.repository.ts index 96c265473..8c1878081 100644 --- a/apps/ecosystem/src/ecosystem.repository.ts +++ b/apps/ecosystem/src/ecosystem.repository.ts @@ -8,6 +8,7 @@ import { SaveSchema, SchemaTransactionResponse, saveCredDef } from '../interface import { ResponseMessages } from '@credebl/common/response-messages'; import { NotFoundException } from '@nestjs/common'; import { CommonConstants } from '@credebl/common/common.constant'; +import { GetAllSchemaList } from '../interfaces/endorsements.interface'; // eslint-disable-next-line camelcase @Injectable() @@ -27,12 +28,13 @@ export class EcosystemRepository { async createNewEcosystem(createEcosystemDto): Promise { try { const transaction = await this.prisma.$transaction(async (prisma) => { - const { name, description, userId, logo, tags, orgId, orgName, orgDid } = createEcosystemDto; + const { name, description, userId, logo, tags, orgId, orgName, orgDid, autoEndorsement } = createEcosystemDto; const createdEcosystem = await prisma.ecosystem.create({ data: { name, description, tags, + autoEndorsement, logoUrl: logo } }); @@ -82,12 +84,13 @@ export class EcosystemRepository { // eslint-disable-next-line camelcase async updateEcosystemById(createEcosystemDto, ecosystemId): Promise { try { - const { name, description, tags, logo } = createEcosystemDto; + const { name, description, tags, logo, autoEndorsement } = createEcosystemDto; const editEcosystem = await this.prisma.ecosystem.update({ where: { id: ecosystemId }, data: { name, description, + autoEndorsement, tags, logoUrl: logo } @@ -151,20 +154,37 @@ export class EcosystemRepository { } } + async checkEcosystemNameExist(name: string): Promise { + try { + return this.prisma.ecosystem.findFirst({ + where: { + name + } + }); + } catch (error) { + this.logger.error(`error: ${JSON.stringify(error)}`); + throw new InternalServerErrorException(error); + } + } + + /** * * @param orgId * @returns Get specific organization details from ecosystem */ // eslint-disable-next-line camelcase - async checkEcosystemOrgs(orgId: string): Promise { + async checkEcosystemOrgs(orgId: string): Promise { try { if (!orgId) { throw new BadRequestException(ResponseMessages.ecosystem.error.invalidOrgId); } - return this.prisma.ecosystem_orgs.findFirst({ + return this.prisma.ecosystem_orgs.findMany({ where: { orgId + }, + include:{ + ecosystemRole: true } }); } catch (error) { @@ -205,6 +225,22 @@ export class EcosystemRepository { } } + // eslint-disable-next-line camelcase + async getSpecificEcosystemConfig(key: string): Promise { + try { + return await this.prisma.ecosystem_config.findFirst( + { + where: { + key + } + } + ); + } catch (error) { + this.logger.error(`error: ${JSON.stringify(error)}`); + throw error; + } + } + async getEcosystemMembersCount(ecosystemId: string): Promise { try { const membersCount = await this.prisma.ecosystem_orgs.count( @@ -549,6 +585,71 @@ export class EcosystemRepository { } } + + async getAllEcosystemSchemasDetails(payload: GetAllSchemaList): Promise<{ + schemasCount: number; + schemasResult: { + createDateTime: Date; + createdBy: number; + name: string; + version: string; + attributes: string; + schemaLedgerId: string; + publisherDid: string; + issuerId: string; + orgId: number; + }[]; + }> { + try { + const { ecosystemId, search, pageNumber, pageSize } = payload; + + const schemaDetails = await this.prisma.endorsement_transaction.findMany({ + where: { + type: endorsementTransactionType.SCHEMA, + status: endorsementTransactionStatus.SUBMITED, + ecosystemOrgs: { + ecosystem: { + id: ecosystemId + } + }, + resourceId: { + not: { + equals: null + } + } + } + }); + const schemaArray = []; + this.logger.error(`In error schemaDetails2: ${JSON.stringify(schemaDetails)}`); + schemaDetails.map((schemaData) => schemaArray.push(schemaData.resourceId)); + const schemasResult = await this.prisma.schema.findMany({ + where: { + OR: [ + { version: { contains: search, mode: 'insensitive' } }, + { name: { contains: search, mode: 'insensitive' } }, + { schemaLedgerId: { contains: search, mode: 'insensitive' } } + ], + schemaLedgerId: { + in: schemaArray + } + }, + take: Number(pageSize), + skip: (pageNumber - 1) * pageSize, + orderBy: { + createDateTime: 'desc' + } + }); + const schemasCount = schemaArray.length; + + this.logger.error(`In error schemaDetails3: ${JSON.stringify(schemasResult)}`); + return { schemasCount, schemasResult }; + + } catch (error) { + this.logger.error(`error: ${JSON.stringify(error)}`); + throw error; + } + } + /** * Description: Get getAgentEndPoint by orgId * @param orgId @@ -655,7 +756,8 @@ export class EcosystemRepository { ecosystemOrgId, responsePayload: '', type, - requestBody + requestBody, + resourceId: '' } }); } catch (error) { @@ -682,12 +784,13 @@ export class EcosystemRepository { // eslint-disable-next-line camelcase - async getEcosystemOrgDetailsbyId(orgId: string): Promise { + async getEcosystemOrgDetailsbyId(orgId: string, ecosystemId: string): Promise { try { //need to change const ecosystemLeadDetails = await this.prisma.ecosystem_orgs.findFirst({ where: { - orgId + orgId, + ecosystemId } }); return ecosystemLeadDetails; @@ -786,6 +889,27 @@ export class EcosystemRepository { } } + async updateResourse( + endorsementId: string, + resourceId: string + // eslint-disable-next-line camelcase, + ): Promise { + try { + const updatedTransaction = await this.prisma.endorsement_transaction.update({ + where: { id: endorsementId }, + data: { + resourceId + } + }); + + return updatedTransaction; + + } catch (error) { + this.logger.error(`Error in updating endorsement transaction: ${error.message}`); + throw error; + } + } + async saveSchema(schemaResult: SaveSchema): Promise { try { const { name, version, attributes, schemaLedgerId, issuerId, createdBy, lastChangedBy, publisherDid, orgId, ledgerId } = schemaResult; diff --git a/apps/ecosystem/src/ecosystem.service.ts b/apps/ecosystem/src/ecosystem.service.ts index 57e4368e8..e97d59f62 100644 --- a/apps/ecosystem/src/ecosystem.service.ts +++ b/apps/ecosystem/src/ecosystem.service.ts @@ -9,12 +9,12 @@ import { EcosystemInviteTemplate } from '../templates/EcosystemInviteTemplate'; import { EmailDto } from '@credebl/common/dtos/email.dto'; import { sendEmail } from '@credebl/common/send-grid-helper-file'; import { AcceptRejectEcosystemInvitationDto } from '../dtos/accept-reject-ecosysteminvitation.dto'; -import { Invitation, OrgAgentType } from '@credebl/enum/enum'; +import { EcosystemConfigSettings, Invitation, OrgAgentType } from '@credebl/enum/enum'; import { EcosystemOrgStatus, EcosystemRoles, endorsementTransactionStatus, endorsementTransactionType } from '../enums/ecosystem.enum'; import { FetchInvitationsPayload } from '../interfaces/invitations.interface'; import { EcosystemMembersPayload } from '../interfaces/ecosystemMembers.interface'; import { CreateEcosystem, CredDefMessage, RequestCredDeffEndorsement, RequestSchemaEndorsement, SaveSchema, SchemaMessage, SignedTransactionMessage, saveCredDef, submitTransactionPayload } from '../interfaces/ecosystem.interfaces'; -import { GetEndorsementsPayload } from '../interfaces/endorsements.interface'; +import { GetAllSchemaList, GetEndorsementsPayload } from '../interfaces/endorsements.interface'; import { CommonConstants } from '@credebl/common/common.constant'; // eslint-disable-next-line camelcase import { credential_definition, org_agents, platform_config, schema, user } from '@prisma/client'; @@ -38,10 +38,24 @@ export class EcosystemService { // eslint-disable-next-line camelcase async createEcosystem(createEcosystemDto: CreateEcosystem): Promise { - const checkOrganization = await this.ecosystemRepository.checkEcosystemOrgs(createEcosystemDto.orgId); - if (checkOrganization) { - throw new ConflictException(ResponseMessages.ecosystem.error.ecosystemOrgAlready); - }; + + const ecosystemExist = await this.ecosystemRepository.checkEcosystemNameExist(createEcosystemDto.name); + + if (ecosystemExist) { + throw new ConflictException(ResponseMessages.ecosystem.error.exists); + } + + const isMultiEcosystemEnabled = await this.ecosystemRepository.getSpecificEcosystemConfig(EcosystemConfigSettings.MULTI_ECOSYSTEM); + + if (isMultiEcosystemEnabled && 'false' === isMultiEcosystemEnabled.value) { + const ecoOrganizationList = await this.ecosystemRepository.checkEcosystemOrgs(createEcosystemDto.orgId); + + for (const organization of ecoOrganizationList) { + if (organization['ecosystemRole']['name'] === EcosystemRoles.ECOSYSTEM_MEMBER) { + throw new ConflictException(ResponseMessages.ecosystem.error.ecosystemOrgAlready); + } + } + } const createEcosystem = await this.ecosystemRepository.createNewEcosystem(createEcosystemDto); if (!createEcosystem) { throw new NotFoundException(ResponseMessages.ecosystem.error.notCreated); @@ -186,11 +200,16 @@ export class EcosystemService { */ async acceptRejectEcosystemInvitations(acceptRejectInvitation: AcceptRejectEcosystemInvitationDto): Promise { try { - const checkOrganization = await this.ecosystemRepository.checkEcosystemOrgs(acceptRejectInvitation.orgId); + const isMultiEcosystemEnabled = await this.ecosystemRepository.getSpecificEcosystemConfig(EcosystemConfigSettings.MULTI_ECOSYSTEM); + if (isMultiEcosystemEnabled + && 'false' === isMultiEcosystemEnabled.value + && acceptRejectInvitation.status !== Invitation.REJECTED) { + const checkOrganization = await this.ecosystemRepository.checkEcosystemOrgs(acceptRejectInvitation.orgId); + if (0 < checkOrganization.length) { + throw new ConflictException(ResponseMessages.ecosystem.error.ecosystemOrgAlready); + }; + } - if (checkOrganization) { - throw new ConflictException(ResponseMessages.ecosystem.error.ecosystemOrgAlready); - }; const { orgId, status, invitationId, orgName, orgDid } = acceptRejectInvitation; const invitation = await this.ecosystemRepository.getEcosystemInvitationById(invitationId); @@ -216,7 +235,7 @@ export class EcosystemService { return ResponseMessages.ecosystem.success.invitationAccept; } catch (error) { - this.logger.error(`acceptRejectInvitations: ${error}`); + this.logger.error(`acceptRejectEcosystemInvitations: ${error}`); throw new RpcException(error.response ? error.response : error); } } @@ -364,7 +383,7 @@ export class EcosystemService { this.ecosystemRepository.getAgentDetails(orgId), this.ecosystemRepository.getPlatformConfigDetails(), this.ecosystemRepository.getAgentDetails(Number(getEcosystemLeadDetails.orgId)), - this.ecosystemRepository.getEcosystemOrgDetailsbyId(String(orgId)) + this.ecosystemRepository.getEcosystemOrgDetailsbyId(String(orgId), ecosystemId) ]); if (0 !== schemaRequestExist.length) { @@ -435,7 +454,7 @@ export class EcosystemService { this.ecosystemRepository.getAgentDetails(orgId), this.ecosystemRepository.getPlatformConfigDetails(), this.ecosystemRepository.getAgentDetails(Number(getEcosystemLeadDetails.orgId)), - this.ecosystemRepository.getEcosystemOrgDetailsbyId(String(orgId)) + this.ecosystemRepository.getEcosystemOrgDetailsbyId(String(orgId), ecosystemId) ]); if (0 !== credDefRequestExist.length) { @@ -484,7 +503,7 @@ export class EcosystemService { throw new NotFoundException(ResponseMessages.ecosystem.error.credentialDefinitionNotFound); } - requestCredDefPayload["credentialDefinition"] = requestBody; + requestCredDefPayload['credentialDefinition'] = requestBody; const schemaTransactionResponse = { endorserDid: ecosystemLeadAgentDetails.orgDid, authorDid: ecosystemMemberDetails.orgDid, @@ -589,11 +608,10 @@ export class EcosystemService { throw new InternalServerErrorException(ResponseMessages.ecosystem.error.signRequestError); } - const autoEndorsement = `${CommonConstants.ECOSYSTEM_AUTO_ENDOSEMENT}`; - const ecosystemConfigDetails = await this.ecosystemRepository.getEcosystemConfigDetails(autoEndorsement); + const ecosystemDetails = await this.ecosystemRepository.getEcosystemDetails(ecosystemId); - if (!ecosystemConfigDetails) { - throw new NotFoundException(ResponseMessages.ecosystem.error.ecosystemConfigNotFound); + if (!ecosystemDetails) { + throw new NotFoundException(ResponseMessages.ecosystem.error.ecosystemNotFound); } const updateSignedTransaction = await this.ecosystemRepository.updateTransactionDetails(endorsementId, schemaTransactionRequest.message.signedTransaction); @@ -602,7 +620,7 @@ export class EcosystemService { throw new InternalServerErrorException(ResponseMessages.ecosystem.error.updateTransactionError); } - if (updateSignedTransaction && 'true' === ecosystemConfigDetails.value) { + if (updateSignedTransaction && true === ecosystemDetails.autoEndorsement) { const submitTxn = await this.submitTransaction(endorsementId, ecosystemId, ecosystemLeadAgentDetails.agentEndPoint); if (!submitTxn) { @@ -792,22 +810,28 @@ export class EcosystemService { const submitTransactionRequest = await this._submitTransaction(payload, url, platformConfig.sgApiKey); - if ('failed' === submitTransactionRequest["message"].state) { + if ('failed' === submitTransactionRequest['message'].state) { throw new InternalServerErrorException(ResponseMessages.ecosystem.error.sumbitTransaction); } await this.updateTransactionStatus(endorsementId); if (endorsementTransactionPayload.type === endorsementTransactionType.SCHEMA) { + + const updateSchemaId = await this._updateResourceId(endorsementId, endorsementTransactionType.SCHEMA, submitTransactionRequest); + + if (!updateSchemaId) { + + throw new InternalServerErrorException(ResponseMessages.ecosystem.error.updateSchemaId); + } return this.handleSchemaSubmission(endorsementTransactionPayload, ecosystemMemberDetails, submitTransactionRequest); } else if (endorsementTransactionPayload.type === endorsementTransactionType.CREDENTIAL_DEFINITION) { - if ('undefined' === submitTransactionRequest["message"].credentialDefinitionId.split(":")[3]) { + if ('undefined' === submitTransactionRequest['message'].credentialDefinitionId.split(':')[3]) { - const autoEndorsement = `${CommonConstants.ECOSYSTEM_AUTO_ENDOSEMENT}`; - const ecosystemConfigDetails = await this.ecosystemRepository.getEcosystemConfigDetails(autoEndorsement); + const ecosystemDetails = await this.ecosystemRepository.getEcosystemDetails(ecosystemId); - if ('true' === ecosystemConfigDetails.value) { + if (true === ecosystemDetails.autoEndorsement) { await this.ecosystemRepository.updateTransactionStatus(endorsementId, endorsementTransactionStatus.REQUESTED); } else { @@ -817,7 +841,12 @@ export class EcosystemService { throw new InternalServerErrorException(ResponseMessages.ecosystem.error.sumbitTransaction); } + const updateCredDefId = await this._updateResourceId(endorsementId, endorsementTransactionType.CREDENTIAL_DEFINITION, submitTransactionRequest); + + if (!updateCredDefId) { + throw new InternalServerErrorException(ResponseMessages.ecosystem.error.updateCredDefId); + } return this.handleCredDefSubmission(endorsementTransactionPayload, ecosystemMemberDetails, submitTransactionRequest); } } catch (error) { @@ -850,6 +879,26 @@ export class EcosystemService { } } + async _updateResourceId(endorsementId: string, transactionType: endorsementTransactionType, transactionDetails: object): Promise { + try { + // eslint-disable-next-line prefer-destructuring + const message = transactionDetails['message']; + if (!message) { + throw new InternalServerErrorException(ResponseMessages.ecosystem.error.invalidMessage); + } + + const resourceId = message[transactionType === endorsementTransactionType.SCHEMA ? 'schemaId' : 'credentialDefinitionId']; + + if (!resourceId) { + throw new Error(`${ResponseMessages.ecosystem.error.invalidTransactionMessage} Missing "${transactionType === endorsementTransactionType.SCHEMA ? 'schemaId' : 'credentialDefinitionId'}" property.`); + } + + return await this.ecosystemRepository.updateResourse(endorsementId, resourceId); + } catch (error) { + this.logger.error(`Error updating resource ID: ${JSON.stringify(error)}`); + } + } + async getAgentUrl( orgAgentTypeId: number, @@ -966,7 +1015,35 @@ export class EcosystemService { return await this.ecosystemRepository.getEndorsementsWithPagination(query, pageNumber, pageSize); } catch (error) { this.logger.error(`In error getEndorsementTransactions: ${JSON.stringify(error)}`); - throw new InternalServerErrorException(error); + throw new RpcException(error.response ? error.response : error); + } + } + + + async getAllEcosystemSchemas(ecosystemSchemas: GetAllSchemaList): Promise { + try { + + const response = await this.ecosystemRepository.getAllEcosystemSchemasDetails(ecosystemSchemas); + this.logger.error(`In error getAllEcosystemSchemas1: ${JSON.stringify(response)}`); + const schemasDetails = response?.schemasResult.map(schemaAttributeItem => { + const attributes = JSON.parse(schemaAttributeItem.attributes); + return { ...schemaAttributeItem, attributes }; + }); + + const schemasResponse = { + totalItems: response.schemasCount, + hasNextPage: ecosystemSchemas.pageSize * ecosystemSchemas.pageNumber < response.schemasCount, + hasPreviousPage: 1 < ecosystemSchemas.pageNumber, + nextPage: ecosystemSchemas.pageNumber + 1, + previousPage: ecosystemSchemas.pageNumber - 1, + lastPage: Math.ceil(response.schemasCount / ecosystemSchemas.pageSize), + data: schemasDetails + }; + this.logger.error(`In error getAllEcosystemSchemas1: ${JSON.stringify(response)}`); + return schemasResponse; + } catch (error) { + this.logger.error(`In error fetching all ecosystem schemas: ${JSON.stringify(error)}`); + throw new RpcException(error.response ? error.response : error); } } @@ -980,7 +1057,7 @@ export class EcosystemService { return await this.ecosystemRepository.updateAutoSignAndSubmitTransaction(); } catch (error) { this.logger.error(`error in decline endorsement request: ${error}`); - throw new InternalServerErrorException(error); + throw new RpcException(error.response ? error.response : error); } } @@ -998,7 +1075,7 @@ export class EcosystemService { return await this.ecosystemRepository.updateEndorsementRequestStatus(ecosystemId, endorsementId); } catch (error) { this.logger.error(`error in decline endorsement request: ${error}`); - throw new InternalServerErrorException(error); + throw new RpcException(error.response ? error.response : error); } } diff --git a/apps/ecosystem/templates/EcosystemInviteTemplate.ts b/apps/ecosystem/templates/EcosystemInviteTemplate.ts index 1486b1db4..d50a18c80 100644 --- a/apps/ecosystem/templates/EcosystemInviteTemplate.ts +++ b/apps/ecosystem/templates/EcosystemInviteTemplate.ts @@ -51,12 +51,12 @@ export class EcosystemInviteTemplate {

In case you need any assistance to access your account, please contact CREDEBL Platform + target="_blank">${process.env.PLATFORM_NAME} Platform


- ® CREDEBL ${year}, Powered by Blockster Labs. All Rights Reserved. + ® ${process.env.PLATFORM_NAME} ${year}, Powered by ${process.env.POWERED_BY}. All Rights Reserved.

diff --git a/apps/issuance/interfaces/issuance.interfaces.ts b/apps/issuance/interfaces/issuance.interfaces.ts index 16aeb6e6c..9ea1d37c1 100644 --- a/apps/issuance/interfaces/issuance.interfaces.ts +++ b/apps/issuance/interfaces/issuance.interfaces.ts @@ -2,7 +2,7 @@ import { IUserRequest } from '@credebl/user-request/user-request.interface'; -export interface IAttributes { +export interface Attributes { name: string; value: string; } @@ -11,7 +11,7 @@ export interface IIssuance { credentialDefinitionId: string; comment: string; connectionId: string; - attributes: IAttributes[]; + attributes: Attributes[]; orgId: number; protocolVersion: string; } @@ -38,9 +38,66 @@ export interface IIssuanceWebhookInterface { credentialAttributes: ICredentialAttributesInterface[]; orgId: number; } - + export interface ICredentialAttributesInterface { 'mime-type': string; name: string; value: string; } + +export interface CredentialOffer { + emailId: string; + attributes: Attributes[]; +} +export interface OutOfBandCredentialOfferPayload { + credentialDefinitionId: string; + orgId: number; + comment?: string; + credentialOffer?: CredentialOffer[]; + emailId?: string; + attributes?: Attributes[]; + protocolVersion?: string; +} + +export interface OutOfBandCredentialOffer { + user: IUserRequest; + outOfBandCredentialDto: OutOfBandCredentialOfferPayload; +} +export interface SchemaDetails { + credentialDefinitionId: string; + tag: string; + schemaLedgerId: string; + attributes: string; +} +export interface ImportFileDetails { + credDefId: string; + filePath: string; + fileName: string; +} + +export interface PreviewRequest { + pageNumber: number, + search: string, + pageSize: number, + sortBy: string, + sortValue: string +} + +export interface FileUpload { + name?: string; + upload_type?: string; + status?: string; + orgId?: number | string; + createDateTime?: Date; + lastChangedDateTime?: Date; + } + +export interface FileUploadData { + fileUpload: string; + fileRow: string; + isError: boolean; + referenceId: string; + createDateTime: Date; + error?: string; + detailError?: string; +} \ No newline at end of file diff --git a/apps/issuance/src/issuance.controller.ts b/apps/issuance/src/issuance.controller.ts index 698ef908b..d3949bb0c 100644 --- a/apps/issuance/src/issuance.controller.ts +++ b/apps/issuance/src/issuance.controller.ts @@ -1,6 +1,6 @@ import { Controller, Logger } from '@nestjs/common'; import { MessagePattern } from '@nestjs/microservices'; -import { IIssuance, IIssuanceWebhookInterface, IIssueCredentials, IIssueCredentialsDefinitions } from '../interfaces/issuance.interfaces'; +import { IIssuance, IIssuanceWebhookInterface, IIssueCredentials, IIssueCredentialsDefinitions, OutOfBandCredentialOffer } from '../interfaces/issuance.interfaces'; import { IssuanceService } from './issuance.service'; @Controller() @@ -37,4 +37,11 @@ export class IssuanceController { const { createDateTime, connectionId, threadId, protocolVersion, credentialAttributes, orgId } = payload; return this.issuanceService.getIssueCredentialWebhook(createDateTime, connectionId, threadId, protocolVersion, credentialAttributes, orgId); } + + @MessagePattern({ cmd: 'out-of-band-credential-offer' }) + async outOfBandCredentialOffer(payload: OutOfBandCredentialOffer): Promise { + const { outOfBandCredentialDto } = payload; + return this.issuanceService.outOfBandCredentialOffer(outOfBandCredentialDto); + } + } diff --git a/apps/issuance/src/issuance.module.ts b/apps/issuance/src/issuance.module.ts index 732e40f35..1bb961269 100644 --- a/apps/issuance/src/issuance.module.ts +++ b/apps/issuance/src/issuance.module.ts @@ -6,6 +6,9 @@ import { ClientsModule, Transport } from '@nestjs/microservices'; import { IssuanceController } from './issuance.controller'; import { IssuanceRepository } from './issuance.repository'; import { IssuanceService } from './issuance.service'; +import { OutOfBandIssuance } from '../templates/out-of-band-issuance.template'; +import { EmailDto } from '@credebl/common/dtos/email.dto'; + @Module({ imports: [ @@ -22,6 +25,6 @@ import { IssuanceService } from './issuance.service'; CommonModule ], controllers: [IssuanceController], - providers: [IssuanceService, IssuanceRepository, PrismaService, Logger] + providers: [IssuanceService, IssuanceRepository, PrismaService, Logger, OutOfBandIssuance, EmailDto] }) export class IssuanceModule { } diff --git a/apps/issuance/src/issuance.repository.ts b/apps/issuance/src/issuance.repository.ts index 0ac80a9e9..a181e92bc 100644 --- a/apps/issuance/src/issuance.repository.ts +++ b/apps/issuance/src/issuance.repository.ts @@ -1,8 +1,10 @@ +/* eslint-disable camelcase */ import { Injectable, InternalServerErrorException, Logger, NotFoundException } from '@nestjs/common'; import { PrismaService } from '@credebl/prisma-service'; // eslint-disable-next-line camelcase -import { agent_invitations, credentials, org_agents, platform_config, shortening_url } from '@prisma/client'; +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'; @Injectable() export class IssuanceRepository { @@ -89,7 +91,7 @@ export class IssuanceRepository { async saveAgentConnectionInvitations(connectionInvitation: string, agentId: number, orgId: number): Promise { try { - const agentDetails = await this.prisma.agent_invitations.create({ + const agentInvitationData = await this.prisma.agent_invitations.create({ data: { orgId, agentId, @@ -97,8 +99,7 @@ export class IssuanceRepository { multiUse: true } }); - return agentDetails; - + return agentInvitationData; } catch (error) { this.logger.error(`Error in saveAgentConnectionInvitations: ${error.message} `); throw error; @@ -115,14 +116,14 @@ export class IssuanceRepository { async storeShorteningUrl(referenceId: string, connectionInvitationUrl: string): Promise { try { - return this.prisma.shortening_url.create({ + const createShorteningUrl = await this.prisma.shortening_url.create({ data: { referenceId, url: connectionInvitationUrl, type: null } }); - + return createShorteningUrl; } catch (error) { this.logger.error(`Error in saveAgentConnectionInvitations: ${error.message} `); throw error; @@ -144,4 +145,114 @@ export class IssuanceRepository { throw new InternalServerErrorException(error); } } + + /** + * Get organization details + * @returns + */ + async getOrganization(orgId: number): Promise { + try { + + const organizationDetails = await this.prisma.organisation.findUnique({ where: { id: orgId } }); + return organizationDetails; + + } catch (error) { + this.logger.error(`[getOrganization] - error: ${JSON.stringify(error)}`); + throw new InternalServerErrorException(error); + } + } + + async getCredentialDefinitionDetails(credentialDefinitionId: string): Promise { + try { + const credentialDefinitionDetails = await this.prisma.credential_definition.findFirst({ + where: { + credentialDefinitionId + } + }); + + if (!credentialDefinitionDetails) { + throw new NotFoundException(`Credential definition not found for ID: ${credentialDefinitionId}`); + } + + const schemaDetails = await this.prisma.schema.findFirst({ + where: { + schemaLedgerId: credentialDefinitionDetails.schemaLedgerId + } + }); + + if (!schemaDetails) { + throw new NotFoundException(`Schema not found for credential definition ID: ${credentialDefinitionId}`); + } + + const credentialDefRes = { + credentialDefinitionId: credentialDefinitionDetails.credentialDefinitionId, + tag: credentialDefinitionDetails.tag, + schemaLedgerId: schemaDetails.schemaLedgerId, + attributes: schemaDetails.attributes + }; + + return credentialDefRes; + } catch (error) { + this.logger.error(`Error in getCredentialDefinitionDetails: ${error.message}`); + throw new InternalServerErrorException(error.message); + } + } + + async saveFileUploadDetails(fileUploadPayload: FileUpload): Promise { + try { + const { name, orgId, status, upload_type } = fileUploadPayload; + return this.prisma.file_upload.create({ + data: { + name, + orgId: `${orgId}`, + status, + upload_type + } + }); + + } catch (error) { + this.logger.error(`[saveFileUploadDetails] - error: ${JSON.stringify(error)}`); + throw error; + } + } + + async updateFileUploadDetails(fileId: string, fileUploadPayload: FileUpload): Promise { + try { + const { name, orgId, status, upload_type } = fileUploadPayload; + return this.prisma.file_upload.update({ + where: { + id: fileId + }, + data: { + name, + orgId: `${orgId}`, + status, + upload_type + } + }); + + } catch (error) { + this.logger.error(`[updateFileUploadDetails] - error: ${JSON.stringify(error)}`); + throw error; + } + } + + async saveFileUploadData(fileUploadData: FileUploadData): Promise { + try { + const { fileUpload, isError, referenceId, error, detailError } = fileUploadData; + 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 1760c97fc..35e653596 100644 --- a/apps/issuance/src/issuance.service.ts +++ b/apps/issuance/src/issuance.service.ts @@ -7,9 +7,13 @@ import { CommonConstants } from '@credebl/common/common.constant'; import { ResponseMessages } from '@credebl/common/response-messages'; import { ClientProxy, RpcException } from '@nestjs/microservices'; import { map } from 'rxjs'; -import { ICredentialAttributesInterface } from '../interfaces/issuance.interfaces'; +import { ICredentialAttributesInterface, OutOfBandCredentialOfferPayload } from '../interfaces/issuance.interfaces'; import { OrgAgentType } from '@credebl/enum/enum'; import { platform_config } from '@prisma/client'; +import * as QRCode from 'qrcode'; +import { OutOfBandIssuance } from '../templates/out-of-band-issuance.template'; +import { EmailDto } from '@credebl/common/dtos/email.dto'; +import { sendEmail } from '@credebl/common/send-grid-helper-file'; @Injectable() @@ -18,8 +22,9 @@ export class IssuanceService { constructor( @Inject('NATS_CLIENT') private readonly issuanceServiceProxy: ClientProxy, private readonly commonService: CommonService, - private readonly issuanceRepository: IssuanceRepository - + private readonly issuanceRepository: IssuanceRepository, + private readonly outOfBandIssuance: OutOfBandIssuance, + private readonly emailData: EmailDto ) { } @@ -94,12 +99,10 @@ export class IssuanceService { } } - async _sendCredentialCreateOffer(issueData: object, url: string, apiKey: string): Promise<{ + async natsCall(pattern: object, payload: object): Promise<{ response: string; }> { try { - const pattern = { cmd: 'agent-send-credential-create-offer' }; - const payload = { issueData, url, apiKey }; return this.issuanceServiceProxy .send(pattern, payload) .pipe( @@ -116,6 +119,19 @@ export class IssuanceService { error: error.message }, error.error); }); + } catch (error) { + this.logger.error(`[natsCall] - error in nats call : ${JSON.stringify(error)}`); + throw new RpcException(error.response ? error.response : error); + } + } + + async _sendCredentialCreateOffer(issueData: object, url: string, apiKey: string): Promise<{ + response: string; + }> { + try { + const pattern = { cmd: 'agent-send-credential-create-offer' }; + const payload = { issueData, url, apiKey }; + return await this.natsCall(pattern, payload); } catch (error) { this.logger.error(`[_sendCredentialCreateOffer] [NATS call]- error in create credentials : ${JSON.stringify(error)}`); throw error; @@ -162,22 +178,7 @@ export class IssuanceService { try { const pattern = { cmd: 'agent-get-all-issued-credentials' }; const payload = { url, apiKey }; - return this.issuanceServiceProxy - .send(pattern, payload) - .pipe( - map((response) => ( - { - response - })) - ).toPromise() - .catch(error => { - this.logger.error(`catch: ${JSON.stringify(error)}`); - throw new HttpException( - { - status: error.statusCode, - error: error.message - }, error.error); - }); + return await this.natsCall(pattern, payload); } catch (error) { this.logger.error(`[_getIssueCredentials] [NATS call]- error in fetch credentials : ${JSON.stringify(error)}`); throw error; @@ -223,28 +224,160 @@ export class IssuanceService { try { const pattern = { cmd: 'agent-get-issued-credentials-by-credentialDefinitionId' }; const payload = { url, apiKey }; - return this.issuanceServiceProxy - .send(pattern, payload) - .pipe( - map((response) => ( - { - response - })) - ).toPromise() - .catch(error => { - this.logger.error(`catch: ${JSON.stringify(error)}`); - throw new HttpException( - { - status: error.statusCode, - error: error.message - }, error.error); - }); + return await this.natsCall(pattern, payload); + } catch (error) { this.logger.error(`[_getIssueCredentialsbyCredentialRecordId] [NATS call]- error in fetch credentials : ${JSON.stringify(error)}`); throw error; } } + + async outOfBandCredentialOffer(outOfBandCredential: OutOfBandCredentialOfferPayload): Promise { + try { + const { + credentialOffer, + comment, + credentialDefinitionId, + orgId, + protocolVersion, + attributes, + emailId + } = outOfBandCredential; + + // Define a batch size + const batchSize = 100; // Adjust this based on your needs + + const agentDetails = await this.issuanceRepository.getAgentEndPoint(orgId); + if (!agentDetails) { + throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); + } + + const issuanceMethodLabel = 'create-offer-oob'; + const url = await this.getAgentUrl(issuanceMethodLabel, agentDetails.orgAgentTypeId, agentDetails.agentEndPoint, agentDetails.tenantId); + const organizationDetails = await this.issuanceRepository.getOrganization(orgId); + + if (!organizationDetails) { + throw new NotFoundException(ResponseMessages.issuance.error.organizationNotFound); + } + + const { apiKey } = agentDetails; + + const errors = []; + const emailPromises = []; + + const sendEmailForCredentialOffer = async (iterator, emailId): Promise => { + try { + const outOfBandIssuancePayload = { + protocolVersion: protocolVersion || 'v1', + credentialFormats: { + indy: { + attributes: iterator.attributes || attributes, + credentialDefinitionId + } + }, + autoAcceptCredential: 'always', + comment + }; + + + const credentialCreateOfferDetails = await this._outOfBandCredentialOffer(outOfBandIssuancePayload, url, apiKey); + if (!credentialCreateOfferDetails) { + errors.push(ResponseMessages.issuance.error.credentialOfferNotFound); + return false; + } + + const invitationId = credentialCreateOfferDetails.response.invitation['@id']; + if (!invitationId) { + errors.push(ResponseMessages.issuance.error.invitationNotFound); + return false; + } + + const agentEndPoint = agentDetails.tenantId + ? `${agentDetails.agentEndPoint}/multi-tenancy/url/${agentDetails.tenantId}/${invitationId}` + : `${agentDetails.agentEndPoint}/url/${invitationId}`; + + const qrCodeOptions = { type: 'image/png' }; + const outOfBandIssuanceQrCode = await QRCode.toDataURL(agentEndPoint, qrCodeOptions); + const platformConfigData = await this.issuanceRepository.getPlatformConfigDetails(); + + if (!platformConfigData) { + errors.push(ResponseMessages.issuance.error.platformConfigNotFound); + return false; + } + + this.emailData.emailFrom = platformConfigData.emailFrom; + this.emailData.emailTo = emailId; + this.emailData.emailSubject = `${process.env.PLATFORM_NAME} Platform: Issuance of Your Credentials`; + this.emailData.emailHtml = await this.outOfBandIssuance.outOfBandIssuance(emailId, organizationDetails.name, outOfBandIssuanceQrCode); + this.emailData.emailAttachments = [ + { + filename: 'qrcode.png', + content: outOfBandIssuanceQrCode.split(';base64,')[1], + contentType: 'image/png', + disposition: 'attachment' + } + ]; + + const isEmailSent = await sendEmail(this.emailData); + if (!isEmailSent) { + errors.push(ResponseMessages.issuance.error.emailSend); + return false; + } + + return isEmailSent; + } catch (error) { + errors.push(error.message); + return false; + } + }; + + if (credentialOffer) { + for (let i = 0; i < credentialOffer.length; i += batchSize) { + const batch = credentialOffer.slice(i, i + batchSize); + + // Process each batch in parallel + const batchPromises = batch.map((iterator) => sendEmailForCredentialOffer(iterator, iterator.emailId)); + + emailPromises.push(Promise.all(batchPromises)); + } + } else { + emailPromises.push(sendEmailForCredentialOffer({}, emailId)); + } + + const results = await Promise.all(emailPromises); + + // Flatten the results array + const flattenedResults = [].concat(...results); + + // Check if all emails were successfully sent + const allSuccessful = flattenedResults.every((result) => true === result); + + if (0 < errors.length) { + throw errors; + } + + return allSuccessful; + } catch (error) { + this.logger.error(`[outOfBoundCredentialOffer] - error in create out-of-band credentials: ${JSON.stringify(error)}`); + throw new RpcException(error); + } + } + + + async _outOfBandCredentialOffer(outOfBandIssuancePayload: object, url: string, apiKey: string): Promise<{ + response; + }> { + try { + const pattern = { cmd: 'agent-out-of-band-credential-offer' }; + const payload = { outOfBandIssuancePayload, url, apiKey }; + return await this.natsCall(pattern, payload); + } catch (error) { + this.logger.error(`[_outOfBandCredentialOffer] [NATS call]- error in out of band : ${JSON.stringify(error)}`); + throw error; + } + } + /** * Description: Fetch agent url * @param referenceId @@ -272,7 +405,7 @@ export class IssuanceService { case 'create-offer-oob': { url = orgAgentTypeId === OrgAgentType.DEDICATED - ? `${agentEndPoint}${CommonConstants.URL_ISSUE_CREATE_CRED_OFFER_AFJ}` + ? `${agentEndPoint}${CommonConstants.URL_OUT_OF_BAND_CREDENTIAL_OFFER}` : orgAgentTypeId === OrgAgentType.SHARED ? `${agentEndPoint}${CommonConstants.URL_SHAGENT_CREATE_OFFER_OUT_OF_BAND}`.replace('#', tenantId) : null; @@ -313,4 +446,5 @@ export class IssuanceService { throw error; } } + } diff --git a/apps/issuance/templates/out-of-band-issuance.template.ts b/apps/issuance/templates/out-of-band-issuance.template.ts new file mode 100644 index 000000000..42ed007f0 --- /dev/null +++ b/apps/issuance/templates/out-of-band-issuance.template.ts @@ -0,0 +1,58 @@ +export class OutOfBandIssuance { + + public outOfBandIssuance(email: string, orgName: string, issuanceQrCode: string): string { + try { + return ` + + + + + + + + + +
+
+ CREDEBL logo +
+
+

+ Hello ${email} , +

+

+ The organization ${orgName} has requested your assistance in issuing your credentials. + We are delighted to notify you that a credential document has been successfully issued to you. To acknowledge and access the document, kindly proceed with the instructions outlined below: +

    +
  • Download the ADEYA Wallet from the Play Store.
  • +
  • Create an Account.
  • +
  • Scan the QR code provided below.
  • +
  • Accept the Credential Document request.
  • +
  • Check your wallet to access the issued Credential Document.
  • +
+ Should you require any assistance or have questions, feel free to contact our dedicated support team. +

+ QR Code +
+
+
+ + f + t +
+

+ Best Regards,The CREDEBL Team +

+
+
+
+ + `; + + } catch (error) { + } + } +} \ No newline at end of file diff --git a/apps/ledger/src/credential-definition/credential-definition.controller.ts b/apps/ledger/src/credential-definition/credential-definition.controller.ts index 1f9aab2f3..6775455af 100644 --- a/apps/ledger/src/credential-definition/credential-definition.controller.ts +++ b/apps/ledger/src/credential-definition/credential-definition.controller.ts @@ -7,6 +7,7 @@ import { MessagePattern } from '@nestjs/microservices'; import { GetAllCredDefsPayload, GetCredDefBySchemaId } from './interfaces/create-credential-definition.interface'; import { CreateCredDefPayload, GetCredDefPayload } from './interfaces/create-credential-definition.interface'; import { credential_definition } from '@prisma/client'; +import { CredDefSchema } from './interfaces/credential-definition.interface'; @Controller('credential-definitions') export class CredentialDefinitionController { @@ -50,4 +51,9 @@ export class CredentialDefinitionController { async getCredentialDefinitionBySchemaId(payload: GetCredDefBySchemaId): Promise { return this.credDefService.getCredentialDefinitionBySchemaId(payload); } + + @MessagePattern({ cmd: 'get-all-schema-cred-defs-for-bulk-operation' }) + async getAllCredDefAndSchemaForBulkOperation (payload: {orgId : number}): Promise { + return this.credDefService.getAllCredDefAndSchemaForBulkOperation(payload.orgId); + } } \ No newline at end of file diff --git a/apps/ledger/src/credential-definition/credential-definition.service.ts b/apps/ledger/src/credential-definition/credential-definition.service.ts index 201bda760..5479ca6e0 100644 --- a/apps/ledger/src/credential-definition/credential-definition.service.ts +++ b/apps/ledger/src/credential-definition/credential-definition.service.ts @@ -12,7 +12,7 @@ import { CredentialDefinitionRepository } from './repositories/credential-defini import { CreateCredDefPayload, CredDefPayload, GetAllCredDefsPayload, GetCredDefBySchemaId, GetCredDefPayload } from './interfaces/create-credential-definition.interface'; import { credential_definition } from '@prisma/client'; import { ResponseMessages } from '@credebl/common/response-messages'; -import { CreateCredDefAgentRedirection, GetCredDefAgentRedirection } from './interfaces/credential-definition.interface'; +import { CreateCredDefAgentRedirection, CredDefSchema, GetCredDefAgentRedirection } from './interfaces/credential-definition.interface'; import { map } from 'rxjs/operators'; @Injectable() @@ -266,4 +266,29 @@ export class CredentialDefinitionService extends BaseService { throw new RpcException(error.response ? error.response : error); } } + + async getAllCredDefAndSchemaForBulkOperation(orgId: number): Promise { + try { + const payload = { + orgId, + sortValue: 'ASC', + credDefSortBy: 'id' + }; + + const credDefSchemaList: CredDefSchema[] = + await this.credentialDefinitionRepository.getAllCredDefsByOrgIdForBulk( + payload + ); + if (!credDefSchemaList) { + throw new NotFoundException(ResponseMessages.credentialDefinition.error.NotFound); + } + return credDefSchemaList; + } catch (error) { + this.logger.error( + `get Cred-Defs and schema List By OrgId for bulk operations: ${JSON.stringify(error)}` + ); + throw new RpcException(error.response); + } + } + } \ No newline at end of file diff --git a/apps/ledger/src/credential-definition/interfaces/credential-definition.interface.ts b/apps/ledger/src/credential-definition/interfaces/credential-definition.interface.ts index 73bbc6c64..2e8baba5e 100644 --- a/apps/ledger/src/credential-definition/interfaces/credential-definition.interface.ts +++ b/apps/ledger/src/credential-definition/interfaces/credential-definition.interface.ts @@ -29,3 +29,14 @@ export interface GetCredDefAgentRedirection { export interface GetCredDefFromTenantPayload { credentialDefinitionId: string; } + +export interface CredDefSchema { + credentialDefinitionId: string; + schemaCredDefName: string; +} + +export interface BulkCredDefSchema { + orgId: number + sortValue: string, + credDefSortBy: string +} 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 12597749b..7710d9f95 100644 --- a/apps/ledger/src/credential-definition/repositories/credential-definition.repository.ts +++ b/apps/ledger/src/credential-definition/repositories/credential-definition.repository.ts @@ -4,6 +4,7 @@ import { PrismaService } from '@credebl/prisma-service'; import { credential_definition, org_agents, org_agents_type, organisation, schema } from '@prisma/client'; import { Injectable, Logger } from '@nestjs/common'; import { ResponseMessages } from '@credebl/common/response-messages'; +import { BulkCredDefSchema, CredDefSchema } from '../interfaces/credential-definition.interface'; @Injectable() export class CredentialDefinitionRepository { @@ -166,4 +167,65 @@ export class CredentialDefinitionRepository { throw error; } } + + + async getAllCredDefsByOrgIdForBulk(payload: BulkCredDefSchema): Promise { + try { + const { credDefSortBy, sortValue, orgId } = payload; + + const credentialDefinitions = await this.prisma.credential_definition.findMany({ + where: { + orgId + }, + select: { + id: true, + credentialDefinitionId: true, + tag: true, + createDateTime: true, + schemaLedgerId: true + }, + orderBy: { + [credDefSortBy]: 'DESC' === sortValue ? 'desc' : 'asc' + } + }); + + const schemaLedgerIdArray = credentialDefinitions.map((credDef) => credDef.schemaLedgerId); + + const schemas = await this.prisma.schema.findMany({ + where: { + schemaLedgerId: { + in: schemaLedgerIdArray + } + }, + select: { + name: true, + version: true, + schemaLedgerId: true, + orgId: true + } + }); + + + // Match Credential Definitions with Schemas and map to CredDefSchema + const matchingSchemas = credentialDefinitions.map((credDef) => { + + const matchingSchema = schemas.find((schema) => schema.schemaLedgerId === credDef.schemaLedgerId); + if (matchingSchema) { + return { + credentialDefinitionId: credDef.credentialDefinitionId, + schemaCredDefName: `${matchingSchema.name}:${matchingSchema.version}-${credDef.tag}` + }; + } + return null; + }); + + // Filter out null values (missing schemas) and return the result + return matchingSchemas.filter((schema) => null !== schema) as CredDefSchema[]; + } catch (error) { + this.logger.error(`Error in listing all credential definitions with schema details: ${error}`); + throw error; + } + } + + } \ No newline at end of file diff --git a/apps/organization/repositories/organization.repository.ts b/apps/organization/repositories/organization.repository.ts index a847a6527..0ec7ca182 100644 --- a/apps/organization/repositories/organization.repository.ts +++ b/apps/organization/repositories/organization.repository.ts @@ -254,6 +254,7 @@ export class OrganizationRepository { ...queryObject }, include: { + schema: true, org_agents: { include: { agents_type: true, @@ -447,4 +448,32 @@ export class OrganizationRepository { } } + async getCredDefByOrg(orgId: number): Promise<{ + tag: string; + credentialDefinitionId: string; + schemaLedgerId: string; + revocable: boolean; + }[]> { + try { + return this.prisma.credential_definition.findMany({ + where: { + orgId + }, + select: { + tag: true, + credentialDefinitionId: true, + schemaLedgerId: true, + revocable: true, + createDateTime: true + }, + orderBy: { + createDateTime: 'desc' + } + }); + } catch (error) { + this.logger.error(`Error in getting agent DID: ${error}`); + throw error; + } + } + } diff --git a/apps/organization/src/organization.service.ts b/apps/organization/src/organization.service.ts index b1f887f18..134ded4ef 100644 --- a/apps/organization/src/organization.service.ts +++ b/apps/organization/src/organization.service.ts @@ -190,6 +190,9 @@ export class OrganizationService { if (!organizationDetails) { throw new NotFoundException(ResponseMessages.organisation.error.profileNotFound); } + + const credentials = await this.organizationRepository.getCredDefByOrg(organizationDetails.id); + organizationDetails['credential_definitions'] = credentials; return organizationDetails; } catch (error) { @@ -296,7 +299,7 @@ export class OrganizationService { } } - /** + /** * * @Body sendInvitationDto * @returns createInvitation @@ -327,7 +330,6 @@ export class OrganizationService { throw new InternalServerErrorException(ResponseMessages.user.error.emailSend); } } - } await this.userActivityService.createActivity(userId, organizationDetails.id, `Invitations sent for ${organizationDetails.name}`, 'Get started with user role management once invitations accepted'); return ResponseMessages.organisation.success.createInvitation; diff --git a/apps/organization/templates/organization-invitation.template.ts b/apps/organization/templates/organization-invitation.template.ts index 8cc0c807c..0ab620803 100644 --- a/apps/organization/templates/organization-invitation.template.ts +++ b/apps/organization/templates/organization-invitation.template.ts @@ -52,13 +52,13 @@ export class OrganizationInviteTemplate { INVITATION -

In case you need any assistance to access your account, please contact CREDEBL Platform +

In case you need any assistance to access your account, please contact ${process.env.PLATFORM_NAME} Platform


- ® CREDEBL ${year}, Powered by Blockster Labs. All Rights Reserved. + ® ${process.env.PLATFORM_NAME} ${year}, Powered by ${process.env.POWERED_BY}. All Rights Reserved.

diff --git a/apps/organization/templates/organization-onboard.template.ts b/apps/organization/templates/organization-onboard.template.ts index 76362276c..22aaa326b 100644 --- a/apps/organization/templates/organization-onboard.template.ts +++ b/apps/organization/templates/organization-onboard.template.ts @@ -7,7 +7,7 @@ export class OnBoardVerificationRequest { return ` - WELCOME TO CREDEBL + WELCOME TO ${process.env.PLATFORM_NAME} @@ -16,7 +16,7 @@ export class OnBoardVerificationRequest {
Credebl Logo
@@ -33,6 +37,7 @@ export class OnBoardVerificationRequest { src="${process.env.API_GATEWAY_PROTOCOL}://${process.env.API_ENDPOINT}/invite.png" alt="Invite Image" style="max-width: 100%" + class="CToWUd" />
The ${orgName} has been sent an onboard request

In case you need any assistance to access your account, please contact - Blockster Labs + ${process.env.POWERED_BY}


@@ -72,7 +77,7 @@ export class OnBoardVerificationRequest { />

- ® CREDEBL ${year}, Powered by Blockster Labs. All Rights Reserved. + ® ${process.env.PLATFORM_NAME} ${year}, Powered by ${process.env.POWERED_BY}. All Rights Reserved.

diff --git a/apps/organization/templates/organization-url-template.ts b/apps/organization/templates/organization-url-template.ts index 58d664f90..d7628bd42 100644 --- a/apps/organization/templates/organization-url-template.ts +++ b/apps/organization/templates/organization-url-template.ts @@ -25,13 +25,10 @@ export class URLOrganizationEmailTemplate { -
+
- Credebl Logo +
-
- verification Image -

@@ -53,8 +50,8 @@ export class URLOrganizationEmailTemplate { VERIFY

-

In case you need any assistance to access your account, please contact Blockster Labs +

In case you need any assistance to access your account, please contact ${process.env.POWERED_BY}


@@ -66,7 +63,7 @@ export class URLOrganizationEmailTemplate { width="18" height="18" alt="t" style="color:#cccccc;">

- ® CREDEBL ${year}, Powered by Blockster Labs. All Rights Reserved. + ® ${process.env.PLATFORM_NAME} ${year}, Powered by ${process.env.POWERED_BY}. All Rights Reserved.

diff --git a/apps/user/interfaces/user.interface.ts b/apps/user/interfaces/user.interface.ts index f42a9b5a8..7ad363215 100644 --- a/apps/user/interfaces/user.interface.ts +++ b/apps/user/interfaces/user.interface.ts @@ -1,5 +1,3 @@ - - export interface UserI { id?: number, username?: string, @@ -54,4 +52,13 @@ export interface UpdateUserProfile { firstName: string, lastName: string, isPublic: boolean, -} \ No newline at end of file +} +export interface PlatformSettingsI { + externalIp: string, + lastInternalId: string, + sgApiKey: string; + emailFrom: string, + apiEndPoint: string; + enableEcosystem: boolean; + multiEcosystemSupport: boolean; +} diff --git a/apps/user/repositories/user.repository.ts b/apps/user/repositories/user.repository.ts index e64f5033a..b8b35b53e 100644 --- a/apps/user/repositories/user.repository.ts +++ b/apps/user/repositories/user.repository.ts @@ -1,7 +1,13 @@ /* eslint-disable prefer-destructuring */ import { Injectable, Logger, NotFoundException } from '@nestjs/common'; -import { UpdateUserProfile, UserEmailVerificationDto, UserI, userInfo } from '../interfaces/user.interface'; +import { + PlatformSettingsI, + UpdateUserProfile, + UserEmailVerificationDto, + UserI, + userInfo +} from '../interfaces/user.interface'; import { InternalServerErrorException } from '@nestjs/common'; import { PrismaService } from '@credebl/prisma-service'; @@ -11,13 +17,16 @@ import { user } from '@prisma/client'; interface UserQueryOptions { id?: number; // Use the appropriate type based on your data model email?: string; // Use the appropriate type based on your data model - username?: string + username?: string; // Add more properties if needed for other unique identifier fields -}; +} @Injectable() export class UserRepository { - constructor(private readonly prisma: PrismaService, private readonly logger: Logger) { } + constructor( + private readonly prisma: PrismaService, + private readonly logger: Logger + ) {} /** * @@ -78,7 +87,6 @@ export class UserRepository { this.logger.error(`Not Found: ${JSON.stringify(error)}`); throw new NotFoundException(error); } - } /** @@ -94,16 +102,15 @@ export class UserRepository { return this.findUser(queryOptions); } - /** + /** * * @param id * @returns User profile data */ async getUserPublicProfile(username: string): Promise { - const queryOptions: UserQueryOptions = { - username - }; + username + }; return this.findUserForPublicProfile(queryOptions); } @@ -114,7 +121,6 @@ export class UserRepository { * @returns Update user profile data */ async updateUserProfile(updateUserProfile: UpdateUserProfile): Promise { - try { const userdetails = await this.prisma.user.update({ where: { @@ -128,7 +134,6 @@ export class UserRepository { } }); return userdetails; - } catch (error) { this.logger.error(`error: ${JSON.stringify(error)}`); throw new InternalServerErrorException(error); @@ -173,7 +178,6 @@ export class UserRepository { this.logger.error(`Not Found: ${JSON.stringify(error)}`); throw new NotFoundException(error); } - } async findUserByEmail(email: string): Promise { @@ -202,7 +206,7 @@ export class UserRepository { firstName: true, lastName: true, profileImg: true, - publicProfile:true, + publicProfile: true, isEmailVerified: true, clientId: true, clientSecret: true, @@ -229,7 +233,7 @@ export class UserRepository { async findUserForPublicProfile(queryOptions: UserQueryOptions): Promise { return this.prisma.user.findFirst({ - where: { + where: { publicProfile: true, OR: [ { @@ -263,7 +267,7 @@ export class UserRepository { website: true, orgSlug: true }, - where:{ + where: { publicProfile: true } } @@ -322,13 +326,17 @@ export class UserRepository { } /** - * - * @param queryOptions - * @param filterOptions + * + * @param queryOptions + * @param filterOptions * @returns users list */ - async findOrgUsers(queryOptions: object, pageNumber: number, pageSize: number, filterOptions?: object): Promise { - + async findOrgUsers( + queryOptions: object, + pageNumber: number, + pageSize: number, + filterOptions?: object + ): Promise { const result = await this.prisma.$transaction([ this.prisma.user.findMany({ where: { @@ -385,51 +393,50 @@ export class UserRepository { return { totalPages, users }; } - /** - * - * @param queryOptions - * @param filterOptions + /** + * + * @param queryOptions + * @param filterOptions * @returns users list */ - async findUsers(queryOptions: object, pageNumber: number, pageSize: number): Promise { + async findUsers(queryOptions: object, pageNumber: number, pageSize: number): Promise { + const result = await this.prisma.$transaction([ + this.prisma.user.findMany({ + where: { + ...queryOptions, // Spread the dynamic condition object + publicProfile: true + }, + select: { + id: true, + username: true, + email: true, + firstName: true, + lastName: true, + profileImg: true, + isEmailVerified: true, + clientId: false, + clientSecret: false, + supabaseUserId: false + }, + take: pageSize, + skip: (pageNumber - 1) * pageSize, + orderBy: { + createDateTime: 'desc' + } + }), + this.prisma.user.count({ + where: { + ...queryOptions + } + }) + ]); - const result = await this.prisma.$transaction([ - this.prisma.user.findMany({ - where: { - ...queryOptions, // Spread the dynamic condition object - publicProfile: true - }, - select: { - id: true, - username: true, - email: true, - firstName: true, - lastName: true, - profileImg: true, - isEmailVerified: true, - clientId: false, - clientSecret: false, - supabaseUserId: false - }, - take: pageSize, - skip: (pageNumber - 1) * pageSize, - orderBy: { - createDateTime: 'desc' - } - }), - this.prisma.user.count({ - where: { - ...queryOptions - } - }) - ]); - - const users = result[0]; - const totalCount = result[1]; - const totalPages = Math.ceil(totalCount / pageSize); - - return { totalPages, users }; - } + const users = result[0]; + const totalCount = result[1]; + const totalPages = Math.ceil(totalCount / pageSize); + + return { totalPages, users }; + } async checkUniqueUserExist(email: string): Promise { try { @@ -461,7 +468,7 @@ export class UserRepository { } } - /** + /** * * @param userInfo * @returns Updates user credentials @@ -484,4 +491,85 @@ export class UserRepository { } } + /** + * + * @Body updatePlatformSettings + * @returns Update platform settings + */ + async updatePlatformSettings(updatePlatformSettings: PlatformSettingsI): Promise { + try { + const getPlatformDetails = await this.prisma.platform_config.findFirst(); + const platformDetails = await this.prisma.platform_config.update({ + where: { + id: getPlatformDetails.id + }, + data: { + externalIp: updatePlatformSettings.externalIp, + lastInternalId: updatePlatformSettings.lastInternalId, + sgApiKey: updatePlatformSettings.sgApiKey, + emailFrom: updatePlatformSettings.emailFrom, + apiEndpoint: updatePlatformSettings.apiEndPoint + } + }); + + return platformDetails; + } catch (error) { + this.logger.error(`error: ${JSON.stringify(error)}`); + throw new InternalServerErrorException(error); + } + } + +/** + * + * @Body updatePlatformSettings + * @returns Update ecosystem settings + */ + 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 + }, + data: { + value: ecosystemObj[key].toString() + } + }); + } + + return true; + + } catch (error) { + this.logger.error(`error: ${JSON.stringify(error)}`); + throw new InternalServerErrorException(error); + } + } + + async getPlatformSettings(): Promise { + try { + const getPlatformSettingsList = await this.prisma.platform_config.findMany(); + return getPlatformSettingsList; + } catch (error) { + this.logger.error(`error in getPlatformSettings: ${JSON.stringify(error)}`); + throw new InternalServerErrorException(error); + } + } + + async getEcosystemSettings(): Promise { + try { + const getEcosystemSettingsList = await this.prisma.ecosystem_config.findMany(); + return getEcosystemSettingsList; + } catch (error) { + this.logger.error(`error in getEcosystemSettings: ${JSON.stringify(error)}`); + throw new InternalServerErrorException(error); + } + } + } diff --git a/apps/user/src/user.controller.ts b/apps/user/src/user.controller.ts index 026da028c..5d3c3b3bb 100644 --- a/apps/user/src/user.controller.ts +++ b/apps/user/src/user.controller.ts @@ -1,4 +1,4 @@ -import { AddPasskeyDetails, UpdateUserProfile, UserEmailVerificationDto, UserI, userInfo } from '../interfaces/user.interface'; +import { AddPasskeyDetails, PlatformSettingsI, UpdateUserProfile, UserEmailVerificationDto, UserI, userInfo } from '../interfaces/user.interface'; import { AcceptRejectInvitationDto } from '../dtos/accept-reject-invitation.dto'; import { Controller } from '@nestjs/common'; @@ -120,4 +120,14 @@ export class UserController { return this.userService.addPasskey(payload.userEmail, payload.userInfo); } + @MessagePattern({ cmd: 'update-platform-settings' }) + async updatePlatformSettings(payload: { platformSettings: PlatformSettingsI }): Promise { + return this.userService.updatePlatformSettings(payload.platformSettings); + } + + @MessagePattern({ cmd: 'fetch-platform-settings' }) + async getPlatformEcosystemSettings(): Promise { + return this.userService.getPlatformEcosystemSettings(); + } + } diff --git a/apps/user/src/user.service.ts b/apps/user/src/user.service.ts index b85d8df94..a8e25762f 100644 --- a/apps/user/src/user.service.ts +++ b/apps/user/src/user.service.ts @@ -1,38 +1,45 @@ - import { BadRequestException, ConflictException, Injectable, Logger, NotFoundException, - UnauthorizedException + UnauthorizedException, + InternalServerErrorException, + Inject, + HttpException } from '@nestjs/common'; import { ClientRegistrationService } from '@credebl/client-registration'; import { CommonService } from '@credebl/common'; import { EmailDto } from '@credebl/common/dtos/email.dto'; -import { InternalServerErrorException } from '@nestjs/common'; import { LoginUserDto } from '../dtos/login-user.dto'; import { OrgRoles } from 'libs/org-roles/enums'; import { OrgRolesService } from '@credebl/org-roles'; import { PrismaService } from '@credebl/prisma-service'; import { ResponseMessages } from '@credebl/common/response-messages'; import { ClientProxy, RpcException } from '@nestjs/microservices'; -import { URLUserEmailTemplate } from '../templates/user-url-template'; +import { URLUserEmailTemplate } from '../templates/user-email-template'; import { UserOrgRolesService } from '@credebl/user-org-roles'; import { UserRepository } from '../repositories/user.repository'; import { VerifyEmailTokenDto } from '../dtos/verify-email.dto'; import { sendEmail } from '@credebl/common/send-grid-helper-file'; -// eslint-disable-next-line camelcase import { user } from '@prisma/client'; -import { Inject } from '@nestjs/common'; -import { HttpException } from '@nestjs/common'; -import { AddPasskeyDetails, InvitationsI, UpdateUserProfile, UserEmailVerificationDto, UserI, userInfo } from '../interfaces/user.interface'; +import { + AddPasskeyDetails, + InvitationsI, + PlatformSettingsI, + UpdateUserProfile, + UserEmailVerificationDto, + UserI, + userInfo +} from '../interfaces/user.interface'; import { AcceptRejectInvitationDto } from '../dtos/accept-reject-invitation.dto'; 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'; @Injectable() export class UserService { @@ -48,7 +55,7 @@ export class UserService { private readonly userDevicesRepository: UserDevicesRepository, private readonly logger: Logger, @Inject('NATS_CLIENT') private readonly userServiceProxy: ClientProxy - ) { } + ) {} /** * @@ -86,7 +93,6 @@ export class UserService { } async createUsername(email: string, verifyCode: string): Promise { - try { // eslint-disable-next-line prefer-destructuring const emailTrim = email.split('@')[0]; @@ -102,7 +108,6 @@ export class UserService { const uniqueUsername = `${cleanedUsername}-${uuid}`; return uniqueUsername; - } catch (error) { this.logger.error(`Error in createUsername: ${JSON.stringify(error)}`); throw new RpcException(error.response ? error.response : error); @@ -127,21 +132,19 @@ export class UserService { emailData.emailTo = email; emailData.emailSubject = `${process.env.PLATFORM_NAME} Platform: Email Verification`; - emailData.emailHtml = await urlEmailTemplate.getUserURLTemplate(email, verificationCode, 'USER'); + emailData.emailHtml = await urlEmailTemplate.getUserURLTemplate(email, verificationCode); const isEmailSent = await sendEmail(emailData); if (isEmailSent) { return isEmailSent; } else { throw new InternalServerErrorException(ResponseMessages.user.error.emailSend); } - } catch (error) { this.logger.error(`Error in sendEmailForVerification: ${JSON.stringify(error)}`); throw new RpcException(error.response ? error.response : error); } } - /** * * @param param email, verification code @@ -178,7 +181,6 @@ export class UserService { } } - async createUserForToken(userInfo: userInfo): Promise { try { const { email } = userInfo; @@ -211,6 +213,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); } @@ -218,11 +221,12 @@ export class UserService { email, password: decryptedPassword }); - } else { + const decryptedPassword = await this.commonService.decryptPassword(userInfo.password); + supaUser = await this.supabaseService.getClient().auth.signUp({ email, - password: userInfo.password + password: decryptedPassword }); } @@ -232,10 +236,7 @@ export class UserService { const supaId = supaUser.data?.user?.id; - await this.userRepository.updateUserDetails( - userDetails.id, - supaId.toString() - ); + await this.userRepository.updateUserDetails(userDetails.id, supaId.toString()); const holderRoleData = await this.orgRoleService.getRole(OrgRoles.HOLDER); await this.userOrgRoleService.createUserOrgRole(userDetails.id, holderRoleData.id); @@ -274,7 +275,6 @@ export class UserService { } } - /** * * @param loginUserDto @@ -300,9 +300,10 @@ export class UserService { 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); } - - return this.generateToken(email, password); } catch (error) { this.logger.error(`In Login User : ${JSON.stringify(error)}`); throw new RpcException(error.response ? error.response : error); @@ -311,14 +312,15 @@ export class UserService { async generateToken(email: string, password: string): Promise { try { - const supaInstance = await this.supabaseService.getClient(); + 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)); if (error) { @@ -335,22 +337,22 @@ export class UserService { async getProfile(payload: { id }): Promise { try { const userData = await this.userRepository.getUserById(payload.id); - const ecosystemDetails = await this.prisma.ecosystem_config.findFirst( + const ecosystemSettingsList = await this.prisma.ecosystem_config.findMany( { where:{ - key: 'enableEcosystem' + OR: [ + { key: EcosystemConfigSettings.ENABLE_ECOSYSTEM }, + { key: EcosystemConfigSettings.MULTI_ECOSYSTEM } + ] } } - ); - - if ('true' === ecosystemDetails.value) { - userData['enableEcosystem'] = true; - return userData; + ); + + for (const setting of ecosystemSettingsList) { + userData[setting.key] = 'true' === setting.value; } - userData['enableEcosystem'] = false; return userData; - } catch (error) { this.logger.error(`get user: ${JSON.stringify(error)}`); throw new RpcException(error.response ? error.response : error); @@ -408,7 +410,7 @@ export class UserService { } } - async invitations(payload: { id; status; pageNumber; pageSize; search; }): Promise { + async invitations(payload: { id; status; pageNumber; pageSize; search }): Promise { try { const userData = await this.userRepository.getUserById(payload.id); @@ -433,10 +435,20 @@ export class UserService { } } - async getOrgInvitations(email: string, status: string, pageNumber: number, pageSize: number, search = ''): Promise { + async getOrgInvitations( + email: string, + status: string, + pageNumber: number, + pageSize: number, + search = '' + ): Promise { const pattern = { cmd: 'fetch-user-invitations' }; const payload = { - email, status, pageNumber, pageSize, search + email, + status, + pageNumber, + pageSize, + search }; const invitationsData = await this.userServiceProxy @@ -534,13 +546,12 @@ export class UserService { } /** - * - * @param orgId + * + * @param orgId * @returns users list */ async getOrgUsers(orgId: number, pageNumber: number, pageSize: number, search: string): Promise { try { - const query = { userOrgRoles: { some: { orgId } @@ -564,10 +575,10 @@ export class UserService { } /** - * - * @param orgId - * @returns users list - */ + * + * @param orgId + * @returns users list + */ async get(pageNumber: number, pageSize: number, search: string): Promise { try { const query = { @@ -589,7 +600,6 @@ export class UserService { try { const userDetails = await this.userRepository.checkUniqueUserExist(email); if (userDetails && !userDetails.isEmailVerified) { - throw new ConflictException(ResponseMessages.user.error.verificationAlreadySent); } else if (userDetails && userDetails.supabaseUserId) { throw new ConflictException(ResponseMessages.user.error.exists); @@ -606,22 +616,84 @@ export class UserService { }; return userVerificationDetails; } - } catch (error) { this.logger.error(`In check User : ${JSON.stringify(error)}`); throw new RpcException(error.response ? error.response : error); } } - async getUserActivity(userId: number, limit: number): Promise { try { - return this.userActivityService.getUserActivity(userId, limit); - } catch (error) { this.logger.error(`In getUserActivity : ${JSON.stringify(error)}`); throw new RpcException(error.response ? error.response : error); } } + + // eslint-disable-next-line camelcase + async updatePlatformSettings(platformSettings: PlatformSettingsI): Promise { + try { + const platformConfigSettings = await this.userRepository.updatePlatformSettings(platformSettings); + + if (!platformConfigSettings) { + throw new BadRequestException(ResponseMessages.user.error.notUpdatePlatformSettings); + } + + const ecosystemobj = {}; + + if (EcosystemConfigSettings.ENABLE_ECOSYSTEM in platformSettings) { + ecosystemobj[EcosystemConfigSettings.ENABLE_ECOSYSTEM] = platformSettings.enableEcosystem; + } + + if (EcosystemConfigSettings.MULTI_ECOSYSTEM in platformSettings) { + ecosystemobj[EcosystemConfigSettings.MULTI_ECOSYSTEM] = platformSettings.multiEcosystemSupport; + } + + const eosystemKeys = Object.keys(ecosystemobj); + + if (0 === eosystemKeys.length) { + return ResponseMessages.user.success.platformEcosystemettings; + } + + const ecosystemSettings = await this.userRepository.updateEcosystemSettings(eosystemKeys, ecosystemobj); + + if (!ecosystemSettings) { + throw new BadRequestException(ResponseMessages.user.error.notUpdateEcosystemSettings); + } + + return ResponseMessages.user.success.platformEcosystemettings; + + } catch (error) { + this.logger.error(`update platform settings: ${JSON.stringify(error)}`); + throw new RpcException(error.response ? error.response : error); + } + } + + async getPlatformEcosystemSettings(): Promise { + try { + + 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); + } + + 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/user-url-template.ts b/apps/user/templates/user-email-template.ts similarity index 67% rename from apps/user/templates/user-url-template.ts rename to apps/user/templates/user-email-template.ts index e8f2234a2..76c0df3f3 100644 --- a/apps/user/templates/user-url-template.ts +++ b/apps/user/templates/user-email-template.ts @@ -1,80 +1,73 @@ -import * as url from 'url'; - -export class URLUserEmailTemplate { - - public getUserURLTemplate(email: string, verificationCode: string, type: string): string { - const endpoint = `${process.env.FRONT_END_URL}`; - const year: number = new Date().getFullYear(); - let apiUrl; - - if ('ADMIN' === type) { - apiUrl = url.parse(`${endpoint}/verify-email-success?verificationCode=${verificationCode}&email=${encodeURIComponent(email)}`); - } else { - apiUrl = url.parse(`${endpoint}/verify-email-success?verificationCode=${verificationCode}&email=${encodeURIComponent(email)}`); - } - const validUrl = apiUrl.href.replace('/:', ':'); - try { - return ` - - - - - - - - - -
-
- Credebl Logo -
-
- verification Image -
-
-

- Hello ${email} , -

-

- Your user account ${email} has been successfully created on ${process.env.PLATFORM_NAME}. In order to enable access for your account, - we need to verify your email address. Please use the link below or click on the “Verify” button to enable access to your account. -

-

Your account details as follows,

-
    -
  • Username/Email: ${email}
  • -
  • Verification Link: ${validUrl}
  • -
- -

In case you need any assistance to access your account, please contact Blockster Labs -

-
-
-
- - f - t -
-

- ® CREDEBL ${year}, Powered by Blockster Labs. All Rights Reserved. -

-
-
-
- - `; - - } catch (error) { - } - } -} - +import * as url from 'url'; +export class URLUserEmailTemplate { + public getUserURLTemplate(email: string, verificationCode: string): string { + const endpoint = `${process.env.FRONT_END_URL}`; + const year: number = new Date().getFullYear(); + + const apiUrl = url.parse( + `${endpoint}/verify-email-success?verificationCode=${verificationCode}&email=${encodeURIComponent(email)}` + ); + + const validUrl = apiUrl.href.replace('/:', ':'); + + try { + return ` + + + + + + + + + +
+
+ CREDEBL logo +
+ +
+

+ Hello ${email} , +

+

+ Your user account ${email} has been successfully created on ${process.env.PLATFORM_NAME}. In order to enable access for your account, + we need to verify your email address. Please use the link below or click on the “Verify” button to enable access to your account. +

+

Your account details as follows,

+
    +
  • Username/Email: ${email}
  • +
  • Verification Link: ${validUrl}
  • +
+ +

In case you need any assistance to access your account, please contact ${process.env.POWERED_BY} +

+
+
+
+ + f + t +
+

+ ® ${process.env.PLATFORM_NAME} ${year}, Powered by ${process.env.POWERED_BY}. All Rights Reserved. +

+
+
+
+ + `; + + } catch (error) {} + } +} diff --git a/apps/user/templates/user-onboard.template.ts b/apps/user/templates/user-onboard.template.ts index 5e8bd62a2..834099b23 100644 --- a/apps/user/templates/user-onboard.template.ts +++ b/apps/user/templates/user-onboard.template.ts @@ -7,7 +7,7 @@ export class OnBoardVerificationRequest { return ` - WELCOME TO CREDEBL + WELCOME TO ${process.env.PLATFORM_NAME} @@ -16,7 +16,7 @@ export class OnBoardVerificationRequest {
Credebl Logo
@@ -49,7 +53,7 @@ export class OnBoardVerificationRequest {

The ${name ? name : email} has been sent an onboard request

In case you need any assistance to access your account, please contact - Blockster Labs + ${process.env.POWERED_BY}


@@ -72,7 +76,7 @@ export class OnBoardVerificationRequest { />

- ® CREDEBL ${year}, Powered by Blockster Labs. All Rights Reserved. + ® ${process.env.PLATFORM_NAME} ${year}, Powered by ${process.env.POWERED_BY}. All Rights Reserved.

diff --git a/apps/verification/src/interfaces/verification.interface.ts b/apps/verification/src/interfaces/verification.interface.ts index 4f25db0f4..99ad821f7 100644 --- a/apps/verification/src/interfaces/verification.interface.ts +++ b/apps/verification/src/interfaces/verification.interface.ts @@ -16,7 +16,7 @@ export interface IRequestProof { comment: string; autoAcceptProof: string; protocolVersion: string; - emailId?: string + emailId?: string[] } export interface IGetAllProofPresentations { diff --git a/apps/verification/src/verification.module.ts b/apps/verification/src/verification.module.ts index 4b6ea652f..d42ce109b 100644 --- a/apps/verification/src/verification.module.ts +++ b/apps/verification/src/verification.module.ts @@ -5,6 +5,8 @@ import { ClientsModule, Transport } from '@nestjs/microservices'; import { CommonModule } from '@credebl/common'; import { VerificationRepository } from './repositories/verification.repository'; import { PrismaService } from '@credebl/prisma-service'; +import { OutOfBandVerification } from '../templates/out-of-band-verification.template'; +import { EmailDto } from '@credebl/common/dtos/email.dto'; @Module({ imports: [ @@ -21,6 +23,6 @@ import { PrismaService } from '@credebl/prisma-service'; CommonModule ], controllers: [VerificationController], - providers: [VerificationService, VerificationRepository, PrismaService, Logger] + providers: [VerificationService, VerificationRepository, PrismaService, Logger, OutOfBandVerification, EmailDto] }) export class VerificationModule { } diff --git a/apps/verification/src/verification.service.ts b/apps/verification/src/verification.service.ts index b7fbad1e0..7d49ff2b2 100644 --- a/apps/verification/src/verification.service.ts +++ b/apps/verification/src/verification.service.ts @@ -5,14 +5,13 @@ import { map } from 'rxjs/operators'; import { IGetAllProofPresentations, IGetProofPresentationById, IProofRequestPayload, IRequestProof, ISendProofRequestPayload, IVerifyPresentation, IWebhookProofPresentation, ProofFormDataPayload } from './interfaces/verification.interface'; import { VerificationRepository } from './repositories/verification.repository'; import { CommonConstants } from '@credebl/common/common.constant'; -import { presentations } from '@prisma/client'; +import { org_agents, organisation, presentations } from '@prisma/client'; import { OrgAgentType } from '@credebl/enum/enum'; import { ResponseMessages } from '@credebl/common/response-messages'; import * as QRCode from 'qrcode'; import { OutOfBandVerification } from '../templates/out-of-band-verification.template'; import { EmailDto } from '@credebl/common/dtos/email.dto'; import { sendEmail } from '@credebl/common/send-grid-helper-file'; -import * as uuid from 'uuid'; @Injectable() export class VerificationService { @@ -21,7 +20,9 @@ export class VerificationService { constructor( @Inject('NATS_CLIENT') private readonly verificationServiceProxy: ClientProxy, - private readonly verificationRepository: VerificationRepository + private readonly verificationRepository: VerificationRepository, + private readonly outOfBandVerification: OutOfBandVerification, + private readonly emailData: EmailDto ) { } @@ -315,114 +316,132 @@ export class VerificationService { } /** - * Request out-of-band proof presentation - * @param outOfBandRequestProof - * @returns Get requested proof presentation details - */ + * Request out-of-band proof presentation + * @param outOfBandRequestProof + * @returns Get requested proof presentation details + */ async sendOutOfBandPresentationRequest(outOfBandRequestProof: IRequestProof): Promise { try { - const comment = outOfBandRequestProof.comment ? outOfBandRequestProof.comment : ''; - - let proofRequestPayload: ISendProofRequestPayload = { - protocolVersion: '', - comment: '', - proofFormats: { - indy: { - name: '', - requested_attributes: {}, - requested_predicates: {}, - version: '' - } - }, - autoAcceptProof: '' - }; + const comment = outOfBandRequestProof.comment || ''; + const protocolVersion = outOfBandRequestProof.protocolVersion || 'v1'; + const autoAcceptProof = outOfBandRequestProof.autoAcceptProof || 'never'; const { requestedAttributes, requestedPredicates } = await this._proofRequestPayload(outOfBandRequestProof); - proofRequestPayload = { - protocolVersion: outOfBandRequestProof.protocolVersion ? outOfBandRequestProof.protocolVersion : 'v1', - comment, - proofFormats: { - indy: { - name: 'Proof Request', - version: '1.0', - // eslint-disable-next-line camelcase - requested_attributes: requestedAttributes, - // eslint-disable-next-line camelcase - requested_predicates: requestedPredicates - } - }, - autoAcceptProof: outOfBandRequestProof.autoAcceptProof ? outOfBandRequestProof.autoAcceptProof : 'never' - }; - const getAgentDetails = await this.verificationRepository.getAgentEndPoint(outOfBandRequestProof.orgId); - const organizationDetails = await this.verificationRepository.getOrganization(outOfBandRequestProof.orgId); + const [getAgentDetails, organizationDetails] = await Promise.all([ + this.verificationRepository.getAgentEndPoint(outOfBandRequestProof.orgId), + this.verificationRepository.getOrganization(outOfBandRequestProof.orgId) + ]); const verificationMethodLabel = 'create-request-out-of-band'; const url = await this.getAgentUrl(verificationMethodLabel, getAgentDetails?.orgAgentTypeId, getAgentDetails?.agentEndPoint, getAgentDetails?.tenantId); - const payload = { apiKey: '', url, proofRequestPayload }; + const payload: IProofRequestPayload + = { + apiKey: '', + url, + proofRequestPayload: { + protocolVersion, + comment, + proofFormats: { + indy: { + name: 'Proof Request', + version: '1.0', + requested_attributes: requestedAttributes, + requested_predicates: requestedPredicates + } + }, + autoAcceptProof + } + }; - const getProofPresentation = await this._sendOutOfBandProofRequest(payload); + const batchSize = 100; // Define the batch size according to your needs + const { emailId } = outOfBandRequestProof; // Assuming it's an array + await this.sendEmailInBatches(payload, emailId, getAgentDetails, organizationDetails, batchSize); + return true; + } catch (error) { + this.logger.error(`[sendOutOfBandPresentationRequest] - error in out of band proof request : ${error.message}`); + throw new RpcException(error.message); + } + } - if (!getProofPresentation) { - throw new NotFoundException(ResponseMessages.verification.error.proofPresentationNotFound); - } + async sendEmailInBatches(payload: IProofRequestPayload, emailIds: string[] | string, getAgentDetails: org_agents, organizationDetails: organisation, batchSize: number): Promise { + const accumulatedErrors = []; - const invitationId = getProofPresentation?.response?.invitation['@id']; + if (Array.isArray(emailIds)) { - if (!invitationId) { - throw new NotFoundException(ResponseMessages.verification.error.invitationNotFound); - } + for (let i = 0; i < emailIds.length; i += batchSize) { + const batch = emailIds.slice(i, i + batchSize); + const emailPromises = batch.map(async email => { + try { + await this.sendOutOfBandProofRequest(payload, email, getAgentDetails, organizationDetails); + } catch (error) { + accumulatedErrors.push(error); + } + }); - let shortenedUrl; - if (getAgentDetails?.tenantId) { - shortenedUrl = `${getAgentDetails?.agentEndPoint}/multi-tenancy/url/${getAgentDetails?.tenantId}/${invitationId}`; - } else { - shortenedUrl = `${getAgentDetails?.agentEndPoint}/url/${invitationId}`; + await Promise.all(emailPromises); } + } else { + await this.sendOutOfBandProofRequest(payload, emailIds, getAgentDetails, organizationDetails); + } - const uniqueCID = uuid.v4(); + if (0 < accumulatedErrors.length) { + this.logger.error(accumulatedErrors); + throw new Error(ResponseMessages.verification.error.emailSend); + } + } - const qrCodeOptions: QRCode.QRCodeToDataURLOptions = { - type: 'image/png' - }; - const outOfBandIssuanceQrCode = await QRCode.toDataURL(shortenedUrl, qrCodeOptions); - const platformConfigData = await this.verificationRepository.getPlatformConfigDetails(); + async sendOutOfBandProofRequest(payload: IProofRequestPayload, email: string, getAgentDetails: org_agents, organizationDetails: organisation): Promise { + const getProofPresentation = await this._sendOutOfBandProofRequest(payload); - if (!platformConfigData) { - throw new NotFoundException(ResponseMessages.verification.error.platformConfigNotFound); - } + if (!getProofPresentation) { + throw new Error(ResponseMessages.verification.error.proofPresentationNotFound); + } - const outOfBandVerification = new OutOfBandVerification(); - const emailData = new EmailDto(); - emailData.emailFrom = platformConfigData.emailFrom; - emailData.emailTo = outOfBandRequestProof.emailId; - emailData.emailSubject = `${process.env.PLATFORM_NAME} Platform: Verification of Your Credentials Required`; - emailData.emailHtml = await outOfBandVerification.outOfBandVerification(outOfBandRequestProof.emailId, uniqueCID, organizationDetails.name); - emailData.emailAttachments = [ - { - filename: 'qrcode.png', - content: outOfBandIssuanceQrCode.split(';base64,')[1], - contentType: 'image/png', - disposition: 'attachment' - } - ]; - const isEmailSent = await sendEmail(emailData); + const invitationId = getProofPresentation?.response?.invitation['@id']; - if (isEmailSent) { - return isEmailSent; - } else { - throw new InternalServerErrorException(ResponseMessages.verification.error.emailSend); + if (!invitationId) { + throw new Error(ResponseMessages.verification.error.invitationNotFound); + } + + const shortenedUrl = getAgentDetails?.tenantId + ? `${getAgentDetails?.agentEndPoint}/multi-tenancy/url/${getAgentDetails?.tenantId}/${invitationId}` + : `${getAgentDetails?.agentEndPoint}/url/${invitationId}`; + + const qrCodeOptions: QRCode.QRCodeToDataURLOptions = { type: 'image/png' }; + const outOfBandVerificationQrCode = await QRCode.toDataURL(shortenedUrl, qrCodeOptions); + + const platformConfigData = await this.verificationRepository.getPlatformConfigDetails(); + + if (!platformConfigData) { + throw new Error(ResponseMessages.verification.error.platformConfigNotFound); + } + + this.emailData.emailFrom = platformConfigData.emailFrom; + this.emailData.emailTo = email; + this.emailData.emailSubject = `${process.env.PLATFORM_NAME} Platform: Verification of Your Credentials`; + this.emailData.emailHtml = await this.outOfBandVerification.outOfBandVerification(email, organizationDetails.name, outOfBandVerificationQrCode); + this.emailData.emailAttachments = [ + { + filename: 'qrcode.png', + content: outOfBandVerificationQrCode.split(';base64,')[1], + contentType: 'image/png', + disposition: 'attachment' } + ]; + const isEmailSent = await sendEmail(this.emailData); - } catch (error) { - this.logger.error(`[sendOutOfBandPresentationRequest] - error in out of band proof request : ${JSON.stringify(error)}`); - throw new RpcException(error.response ? error.response : error); + if (!isEmailSent) { + throw new Error(ResponseMessages.verification.error.emailSend); } + + return isEmailSent; } + /** * Consume agent API for request out-of-band proof presentation * @param payload diff --git a/apps/verification/templates/out-of-band-verification.template.ts b/apps/verification/templates/out-of-band-verification.template.ts index 992cacf08..b4978aa57 100644 --- a/apps/verification/templates/out-of-band-verification.template.ts +++ b/apps/verification/templates/out-of-band-verification.template.ts @@ -1,6 +1,6 @@ export class OutOfBandVerification { - public outOfBandVerification(email: string, uniqueCID: string, orgName: string): string { + public outOfBandVerification(email: string, orgName: string, verificationQrCode: string): string { try { return ` @@ -12,13 +12,10 @@ export class OutOfBandVerification { -
+
- Credebl Logo + CREDEBL logo
-
- verification Image -

@@ -28,7 +25,7 @@ export class OutOfBandVerification { The organization ${orgName} has requested your assistance in verifying your credentials. To proceed, kindly follow the steps outlined below:

    -
  • Download the ADHAYA Wallet application from the Play Store.
  • +
  • Download the ADEYA Wallet application from the Play Store.
  • Create a new account within the app.
  • Scan the QR code provided below within the app.
  • Accept the request for the Credential Document.
  • @@ -37,6 +34,7 @@ export class OutOfBandVerification {
Should you encounter any difficulties or have inquiries, our dedicated support team is available to assist you. Feel free to reach out.

+ QR Code