diff --git a/Dockerfiles/Dockerfile.cloud-wallet b/Dockerfiles/Dockerfile.cloud-wallet new file mode 100644 index 000000000..1ddb48e3c --- /dev/null +++ b/Dockerfiles/Dockerfile.cloud-wallet @@ -0,0 +1,43 @@ +# Stage 1: Build the application +FROM node:18-slim as build +RUN npm install -g pnpm + +RUN apt-get update -y +RUN apt-get --no-install-recommends install -y openssl +# Set the working directory +WORKDIR /app + +# Copy package.json and package-lock.json +COPY package.json ./ + +# Install dependencies +RUN pnpm install + +# Copy the rest of the application code +COPY . . +# RUN cd libs/prisma-service && npx prisma migrate deploy && npx prisma generate +RUN cd libs/prisma-service && npx prisma migrate deploy && npx prisma generate + +# Build the user service +RUN pnpm run build cloud-wallet + +# Stage 2: Create the final image +FROM node:18-slim + +RUN apt-get update -y +RUN apt-get --no-install-recommends install -y openssl +# Set the working directory +WORKDIR /app +RUN npm install -g pnpm + +# Copy the compiled code from the build stage +COPY --from=build /app/dist/apps/cloud-wallet/ ./dist/apps/cloud-wallet/ + +# Copy the libs folder from the build stage +COPY --from=build /app/libs/ ./libs/ +#COPY --from=build /app/package.json ./ +COPY --from=build /app/node_modules ./node_modules + + +# Set the command to run the microservice +CMD ["sh", "-c", "cd libs/prisma-service && npx prisma migrate deploy && cd ../.. && node dist/apps/cloud-wallet/main.js"] diff --git a/apps/api-gateway/src/app.module.ts b/apps/api-gateway/src/app.module.ts index 06a918c97..42a252563 100644 --- a/apps/api-gateway/src/app.module.ts +++ b/apps/api-gateway/src/app.module.ts @@ -28,6 +28,7 @@ import { UtilitiesModule } from './utilities/utilities.module'; import { NotificationModule } from './notification/notification.module'; import { GeoLocationModule } from './geo-location/geo-location.module'; import { CommonConstants } from '@credebl/common/common.constant'; +import { CloudWalletModule } from './cloud-wallet/cloud-wallet.module'; @Module({ imports: [ @@ -56,7 +57,8 @@ import { CommonConstants } from '@credebl/common/common.constant'; WebhookModule, NotificationModule, CacheModule.register({ store: redisStore, host: process.env.REDIS_HOST, port: process.env.REDIS_PORT }), - GeoLocationModule + GeoLocationModule, + CloudWalletModule ], controllers: [AppController], providers: [AppService] 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 f3c1178d1..a5b9c5119 100644 --- a/apps/api-gateway/src/authz/guards/org-roles.guard.ts +++ b/apps/api-gateway/src/authz/guards/org-roles.guard.ts @@ -26,6 +26,10 @@ export class OrgRolesGuard implements CanActivate { const req = context.switchToHttp().getRequest(); const { user } = req; + if (user?.userRole && user?.userRole.includes('holder')) { + throw new ForbiddenException('This role is a holder.'); + } + req.params.orgId = req.params?.orgId ? req.params?.orgId?.trim() : ''; req.query.orgId = req.query?.orgId ? req.query?.orgId?.trim() : ''; req.body.orgId = req.body?.orgId ? req.body?.orgId?.trim() : ''; diff --git a/apps/api-gateway/src/authz/guards/user-access-guard.ts b/apps/api-gateway/src/authz/guards/user-access-guard.ts index ea53f63fc..95f37db58 100644 --- a/apps/api-gateway/src/authz/guards/user-access-guard.ts +++ b/apps/api-gateway/src/authz/guards/user-access-guard.ts @@ -1,4 +1,4 @@ -import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common'; +import { Injectable, CanActivate, ExecutionContext, UnauthorizedException, ForbiddenException } from '@nestjs/common'; import { Observable } from 'rxjs'; @Injectable() @@ -11,6 +11,12 @@ export class UserAccessGuard implements CanActivate { if (user.hasOwnProperty('client_id')) { throw new UnauthorizedException('You do not have access'); } + + if (user?.userRole && user?.userRole.includes('holder')) { + throw new ForbiddenException('This role is a holder.'); + } + + return true; } } diff --git a/apps/api-gateway/src/authz/guards/user-role.guard.ts b/apps/api-gateway/src/authz/guards/user-role.guard.ts new file mode 100644 index 000000000..3c3dc4d7a --- /dev/null +++ b/apps/api-gateway/src/authz/guards/user-role.guard.ts @@ -0,0 +1,21 @@ +import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common'; +import { Observable } from 'rxjs'; + +@Injectable() +export class UserRoleGuard implements CanActivate { + canActivate(context: ExecutionContext): boolean | Promise | Observable { + const request = context.switchToHttp().getRequest(); + + const { user } = request; + + if (!user?.userRole) { + throw new ForbiddenException('This role is not a holder.'); + } + + if (!user?.userRole.includes('holder')) { + throw new ForbiddenException('This role is not a holder.'); + } + + return true; + } +} diff --git a/apps/api-gateway/src/authz/jwt-payload.interface.ts b/apps/api-gateway/src/authz/jwt-payload.interface.ts index 0cdf673df..c4dcec845 100644 --- a/apps/api-gateway/src/authz/jwt-payload.interface.ts +++ b/apps/api-gateway/src/authz/jwt-payload.interface.ts @@ -8,5 +8,6 @@ export interface JwtPayload { scope: string; gty?: string; permissions: string[]; + email?: string } \ No newline at end of file diff --git a/apps/api-gateway/src/authz/jwt.strategy.ts b/apps/api-gateway/src/authz/jwt.strategy.ts index 8bedcbc54..683bc08ac 100644 --- a/apps/api-gateway/src/authz/jwt.strategy.ts +++ b/apps/api-gateway/src/authz/jwt.strategy.ts @@ -56,6 +56,7 @@ export class JwtStrategy extends PassportStrategy(Strategy) { let userDetails = null; + const userInfo = await this.usersService.getUserByUserIdInKeycloak(payload.email); if (payload.hasOwnProperty('client_id')) { const orgDetails: IOrganization = await this.organizationService.findOrganizationOwner(payload['client_id']); @@ -87,6 +88,10 @@ export class JwtStrategy extends PassportStrategy(Strategy) { throw new NotFoundException(ResponseMessages.user.error.notFound); } + if (userInfo && userInfo?.['attributes'] && userInfo?.['attributes']?.userRole) { + userDetails['userRole'] = userInfo?.['attributes']?.userRole; + } + return { ...userDetails, ...payload diff --git a/apps/api-gateway/src/cloud-wallet/cloud-wallet.controller.ts b/apps/api-gateway/src/cloud-wallet/cloud-wallet.controller.ts new file mode 100644 index 000000000..449e746fa --- /dev/null +++ b/apps/api-gateway/src/cloud-wallet/cloud-wallet.controller.ts @@ -0,0 +1,532 @@ +import { IResponse } from '@credebl/common/interfaces/response.interface'; +import { ResponseMessages } from '@credebl/common/response-messages'; +import { Controller, Post, Logger, Body, HttpStatus, Res, UseFilters, UseGuards, Get, Param, Query, BadRequestException } from '@nestjs/common'; +import { ApiBearerAuth, ApiForbiddenResponse, ApiOperation, ApiQuery, ApiResponse, ApiTags, ApiUnauthorizedResponse } from '@nestjs/swagger'; +import { ForbiddenErrorDto } from '../dtos/forbidden-error.dto'; +import { UnauthorizedErrorDto } from '../dtos/unauthorized-error.dto'; +import { CloudWalletService } from './cloud-wallet.service'; +import { AcceptOfferDto, BasicMessageDTO, CreateCloudWalletDidDto, CreateCloudWalletDto, CredentialListDto, GetAllCloudWalletConnectionsDto, ReceiveInvitationUrlDTO } from './dtos/cloudWallet.dto'; +import { Response } from 'express'; +import { CustomExceptionFilter } from 'apps/api-gateway/common/exception-handler'; +import { ApiResponseDto } from '../dtos/apiResponse.dto'; +import { CloudBaseWalletConfigureDto } from './dtos/configure-base-wallet.dto'; +import { AuthGuard } from '@nestjs/passport'; +import { User } from '../authz/decorators/user.decorator'; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import { user } from '@prisma/client'; +import { validateDid } from '@credebl/common/did.validator'; +import { CommonConstants } from '@credebl/common/common.constant'; +import { UserRoleGuard } from '../authz/guards/user-role.guard'; +import { AcceptProofRequestDto } from './dtos/accept-proof-request.dto'; +import { IBasicMessage, IConnectionDetailsById, ICredentialDetails, IGetProofPresentation, IGetProofPresentationById, IWalletDetailsForDidList } from '@credebl/common/interfaces/cloud-wallet.interface'; +import { CreateConnectionDto } from './dtos/create-connection.dto'; + + +@UseFilters(CustomExceptionFilter) +@Controller() +@ApiTags('cloud-wallet') +@ApiBearerAuth() +@ApiUnauthorizedResponse({ status: HttpStatus.UNAUTHORIZED, description: 'Unauthorized', type: UnauthorizedErrorDto }) +@ApiForbiddenResponse({ status: HttpStatus.FORBIDDEN, description: 'Forbidden', type: ForbiddenErrorDto }) +export class CloudWalletController { + + private readonly logger = new Logger('cloud-wallet'); + constructor(private readonly cloudWalletService: CloudWalletService + ) { } + + /** + * Configure cloud base wallet + * @param cloudBaseWalletConfigure + * @param user + * @param res + * @returns sucess message + */ + @Post('/configure/base-wallet') + @ApiOperation({ summary: 'Configure base wallet', description: 'Configure base wallet' }) + @ApiResponse({ status: HttpStatus.CREATED, description: 'Created', type: ApiResponseDto }) + @UseGuards(AuthGuard('jwt')) + async configureBaseWallet( + @Res() res: Response, + @Body() cloudBaseWalletConfigure: CloudBaseWalletConfigureDto, + @User() user: user + ): Promise { + + const { id, email } = user; + + cloudBaseWalletConfigure.userId = id; + cloudBaseWalletConfigure.email = email; + + const configureBaseWalletData = await this.cloudWalletService.configureBaseWallet(cloudBaseWalletConfigure); + const finalResponse: IResponse = { + statusCode: HttpStatus.CREATED, + message: ResponseMessages.cloudWallet.success.configureBaseWallet, + data: configureBaseWalletData + }; + return res.status(HttpStatus.CREATED).json(finalResponse); + } + + /** + * Create cloud wallet + * @param cloudWalletDetails + * @param res + * @returns Sucess message and wallet details + */ + @Post('/create-wallet') + @ApiOperation({ summary: 'Create cloud wallet', description: 'Create cloud wallet' }) + @ApiResponse({ status: HttpStatus.CREATED, description: 'Created', type: ApiResponseDto }) + @ApiBearerAuth() + @UseGuards(AuthGuard('jwt'), UserRoleGuard) + async createCloudWallet( + @Res() res: Response, + @Body() cloudWalletDetails: CreateCloudWalletDto, + @User() user: user + ): Promise { + const {email, id} = user; + cloudWalletDetails.email = email; + cloudWalletDetails.userId = id; + const cloudWalletData = await this.cloudWalletService.createCloudWallet(cloudWalletDetails); + const finalResponse: IResponse = { + statusCode: HttpStatus.CREATED, + message: ResponseMessages.cloudWallet.success.create, + data: cloudWalletData + }; + return res.status(HttpStatus.CREATED).json(finalResponse); + + } + + + /** + * Accept proof request + * @param acceptProofRequest + * @returns sucess message + */ + @Post('/proofs/accept-request') + @ApiOperation({ summary: 'Accept proof request', description: 'Accept proof request' }) + @ApiResponse({ status: HttpStatus.CREATED, description: 'Created', type: ApiResponseDto }) + @UseGuards(AuthGuard('jwt'), UserRoleGuard) + async acceptProofRequest( + @Res() res: Response, + @Body() acceptProofRequest: AcceptProofRequestDto, + @User() user: user + ): Promise { + const { id, email } = user; + acceptProofRequest.userId = id; + acceptProofRequest.email = email; + + const acceptProofRequestDetails = await this.cloudWalletService.acceptProofRequest(acceptProofRequest); + const finalResponse: IResponse = { + statusCode: HttpStatus.CREATED, + message: ResponseMessages.cloudWallet.success.acceptProofRequest, + data: acceptProofRequestDetails + }; + return res.status(HttpStatus.CREATED).json(finalResponse); + } + + /** + * Get proof presentation by proof id + * @param proofRecordId + * @param res + * @returns sucess message + */ + @Get('/proofs/:proofRecordId') + @ApiOperation({ summary: 'Get proof presentation by Id', description: 'Get proof presentation by Id' }) + @ApiResponse({ status: HttpStatus.OK, description: 'Success', type: ApiResponseDto }) + @UseGuards(AuthGuard('jwt'), UserRoleGuard) + async getProofById( + @Param('proofRecordId') proofRecordId: string, + @Res() res: Response, + @User() user: user + ): Promise { + const { id, email } = user; + + const proofPresentationByIdPayload: IGetProofPresentationById = { + userId: id, + email, + proofRecordId + }; + + const getProofDetails = await this.cloudWalletService.getProofById(proofPresentationByIdPayload); + const finalResponse: IResponse = { + statusCode: HttpStatus.OK, + message: ResponseMessages.cloudWallet.success.getProofById, + data: getProofDetails + }; + return res.status(HttpStatus.OK).json(finalResponse); + } + + /** + * Get proof presentations + * @param threadId + * @param res + * @returns sucess message + */ + @Get('/proofs') + @ApiOperation({ summary: 'Get proof presentation', description: 'Get proof presentation' }) + @ApiResponse({ status: HttpStatus.OK, description: 'Success', type: ApiResponseDto }) + @UseGuards(AuthGuard('jwt'), UserRoleGuard) + @ApiQuery({ + name: 'threadId', + required: false + }) + async getProofPresentation( + @Res() res: Response, + @User() user: user, + @Query('threadId') threadId?: string + ): Promise { + + const { id, email } = user; + + const proofPresentationPayload: IGetProofPresentation = { + userId: id, + email, + threadId + }; + + const getProofDetails = await this.cloudWalletService.getProofPresentation(proofPresentationPayload); + const finalResponse: IResponse = { + statusCode: HttpStatus.OK, + message: ResponseMessages.cloudWallet.success.getProofPresentation, + data: getProofDetails + }; + return res.status(HttpStatus.OK).json(finalResponse); + } + + /** + * Receive invitation by URL + * @param receiveInvitation + * @param res + * @returns Response from agent + */ + @Post('/receive-invitation-url') + @ApiOperation({ summary: 'Receive inviation using URL', description: 'Receive inviation using URL' }) + @ApiResponse({ status: HttpStatus.CREATED, description: 'Created', type: ApiResponseDto }) + @ApiBearerAuth() + @UseGuards(AuthGuard('jwt'), UserRoleGuard) + async receiveInvitationByUrl( + @Res() res: Response, + @Body() receiveInvitation: ReceiveInvitationUrlDTO, + @User() user: user + ): Promise { + const {email, id} = user; + receiveInvitation.email = email; + receiveInvitation.userId = id; + const receiveInvitationData = await this.cloudWalletService.receiveInvitationByUrl(receiveInvitation); + const finalResponse: IResponse = { + statusCode: HttpStatus.CREATED, + message: ResponseMessages.cloudWallet.success.receive, + data: receiveInvitationData + }; + return res.status(HttpStatus.CREATED).json(finalResponse); + + } + + /** + * Accept offer + * @param acceptOffer + * @param res + * @returns Response from agent + */ + @Post('/accept-offer') + @ApiOperation({ summary: 'Accept credential offer', description: 'Accept credential offer' }) + @ApiResponse({ status: HttpStatus.CREATED, description: 'Created', type: ApiResponseDto }) + @ApiBearerAuth() + @UseGuards(AuthGuard('jwt'), UserRoleGuard) + async acceptOffer( + @Res() res: Response, + @Body() acceptOffer: AcceptOfferDto, + @User() user: user + ): Promise { + const {email, id} = user; + acceptOffer.email = email; + acceptOffer.userId = id; + const receiveInvitationData = await this.cloudWalletService.acceptOffer(acceptOffer); + const finalResponse: IResponse = { + statusCode: HttpStatus.CREATED, + message: ResponseMessages.cloudWallet.success.receive, + data: receiveInvitationData + }; + return res.status(HttpStatus.CREATED).json(finalResponse); + + } + + /** + * Create did + * @param orgId + * @returns did + */ + @Post('/did') + @ApiOperation({ + summary: 'Create new did', + description: 'Create new did for cloud wallet' + }) + @ApiBearerAuth() + @UseGuards(AuthGuard('jwt'), UserRoleGuard) + @ApiResponse({ status: HttpStatus.CREATED, description: 'Success', type: ApiResponseDto }) + async createDid( + @Body() createDidDto: CreateCloudWalletDidDto, + @User() user: user, + @Res() res: Response + ): Promise { + await validateDid(createDidDto); + const {email, id} = user; + createDidDto.email = email; + createDidDto.userId = id; + if (createDidDto.seed && CommonConstants.SEED_LENGTH !== createDidDto.seed.length) { + throw new BadRequestException(ResponseMessages.agent.error.seedChar, { + cause: new Error(), + description: ResponseMessages.errorMessages.badRequest + }); + } + + const didDetails = await this.cloudWalletService.createDid(createDidDto); + + const finalResponse: IResponse = { + statusCode: HttpStatus.CREATED, + message: ResponseMessages.agent.success.createDid, + data: didDetails + }; + + return res.status(HttpStatus.CREATED).json(finalResponse); + } + + /** + * Get DID list by tenant id + * @param tenantId + * @param res + * @returns DID list + */ + @Get('/did') + @ApiOperation({ summary: 'Get DID list from wallet', description: 'Get DID list from wallet' }) + @ApiResponse({ status: HttpStatus.OK, description: 'Success', type: ApiResponseDto }) + @UseGuards(AuthGuard('jwt'), UserRoleGuard) + async getDidList( + @Res() res: Response, + @User() user: user + ): Promise { + const { id, email } = user; + + const walletDetails: IWalletDetailsForDidList = { + userId: id, + email + }; + + const didListDetails = await this.cloudWalletService.getDidList(walletDetails); + const finalResponse: IResponse = { + statusCode: HttpStatus.OK, + message: ResponseMessages.cloudWallet.success.didList, + data: didListDetails + }; + return res.status(HttpStatus.OK).json(finalResponse); + } + + /** + * Accept proof request + * @param CreateConnectionDto + * @returns sucess message + */ + @Post('/connections/invitation') + @ApiOperation({ summary: 'Create connection invitation for cloud wallet', description: 'Create connection invitation' }) + @ApiResponse({ status: HttpStatus.CREATED, description: 'Created', type: ApiResponseDto }) + @UseGuards(AuthGuard('jwt'), UserRoleGuard) + async createConnection( + @Res() res: Response, + @Body() createConnection: CreateConnectionDto, + @User() user: user + ): Promise { + const { id, email } = user; + createConnection.userId = id; + createConnection.email = email; + + const createConnectionDetails = await this.cloudWalletService.createConnection(createConnection); + const finalResponse: IResponse = { + statusCode: HttpStatus.CREATED, + message: ResponseMessages.cloudWallet.success.createConnection, + data: createConnectionDetails + }; + return res.status(HttpStatus.CREATED).json(finalResponse); + } + + /** + * Get connection list by tenant id and connection id + * @param tenantId + * @param connectionId + * @param res + * @returns DID list + */ + @Get('/connection/:connectionId') + @ApiOperation({ summary: 'Get connection by connection Id', description: 'Get connection by connection Id' }) + @ApiResponse({ status: HttpStatus.OK, description: 'Success', type: ApiResponseDto }) + @UseGuards(AuthGuard('jwt'), UserRoleGuard) + async getconnectionById( + @Param('connectionId') connectionId: string, + @Res() res: Response, + @User() user: user + ): Promise { + const { id, email } = user; + + const connectionDetails: IConnectionDetailsById = { + userId: id, + email, + connectionId + }; + + const connectionDetailResponse = await this.cloudWalletService.getconnectionById(connectionDetails); + const finalResponse: IResponse = { + statusCode: HttpStatus.OK, + message: ResponseMessages.cloudWallet.success.connectionById, + data: connectionDetailResponse + }; + return res.status(HttpStatus.OK).json(finalResponse); + } + + /** + * Get connection list by tenant id + * @param res + * @returns DID list + */ + @Get('/connections') + @ApiOperation({ summary: 'Get all wallet connections', description: 'Get all wallet connections' }) + @ApiResponse({ status: HttpStatus.OK, description: 'Success', type: ApiResponseDto }) + @UseGuards(AuthGuard('jwt'), UserRoleGuard) + async getAllconnectionById( + @Query() connectionListQueryOptions: GetAllCloudWalletConnectionsDto, + @Res() res: Response, + @User() user: user + ): Promise { + const { id, email } = user; + + connectionListQueryOptions.userId = id; + connectionListQueryOptions.email = email; + + const connectionDetailResponse = await this.cloudWalletService.getAllconnectionById(connectionListQueryOptions); + const finalResponse: IResponse = { + statusCode: HttpStatus.OK, + message: ResponseMessages.cloudWallet.success.connectionList, + data: connectionDetailResponse + }; + return res.status(HttpStatus.OK).json(finalResponse); + } + + /** + * Get credential list by tenant id + * @param credentialListQueryOptions + * @param res + * @returns Credential list + */ + @Get('/credential') + @ApiOperation({ summary: 'Get credential list from cloud wallet', description: 'Get credential list from cloud wallet' }) + @ApiResponse({ status: HttpStatus.OK, description: 'Success', type: ApiResponseDto }) + @UseGuards(AuthGuard('jwt'), UserRoleGuard) + async getCredentialList( + @Query() credentialListQueryOptions: CredentialListDto, + @Res() res: Response, + @User() user: user + ): Promise { + const { id, email } = user; + + credentialListQueryOptions.userId = id; + credentialListQueryOptions.email = email; + + const connectionDetailResponse = await this.cloudWalletService.getCredentialList(credentialListQueryOptions); + const finalResponse: IResponse = { + statusCode: HttpStatus.OK, + message: ResponseMessages.cloudWallet.success.credentials, + data: connectionDetailResponse + }; + return res.status(HttpStatus.OK).json(finalResponse); + } + + /** + * Get credential list by tenant id + * @param credentialListQueryOptions + * @param res + * @returns Credential list + */ + @Get('/credential/:credentialRecordId') + @ApiOperation({ summary: 'Get credential by credential record Id', description: 'Get credential by credential record Id' }) + @ApiResponse({ status: HttpStatus.OK, description: 'Success', type: ApiResponseDto }) + @UseGuards(AuthGuard('jwt'), UserRoleGuard) + async getCredentialByCredentialRecordId( + @Param('credentialRecordId') credentialRecordId: string, + @Res() res: Response, + @User() user: user + ): Promise { + const { id, email } = user; + + const credentialDetails: ICredentialDetails = { + userId: id, + email, + credentialRecordId + }; + + const connectionDetailResponse = await this.cloudWalletService.getCredentialByCredentialRecordId(credentialDetails); + const finalResponse: IResponse = { + statusCode: HttpStatus.OK, + message: ResponseMessages.cloudWallet.success.credentialByRecordId, + data: connectionDetailResponse + }; + return res.status(HttpStatus.OK).json(finalResponse); + } + + /** + * Get basic-message by connection id + * @param connectionId + * @param res + * @returns Credential list + */ + @Get('/basic-message/:connectionId') + @ApiOperation({ summary: 'Get basic message by connection id', description: 'Get basic message by connection id' }) + @ApiResponse({ status: HttpStatus.OK, description: 'Success', type: ApiResponseDto }) + @UseGuards(AuthGuard('jwt'), UserRoleGuard) + async getBasicMessageByConnectionId( + @Param('connectionId') connectionId: string, + @Res() res: Response, + @User() user: user + ): Promise { + const { id, email } = user; + + const connectionDetails: IBasicMessage = { + userId: id, + email, + connectionId + }; + + const basicMessageDetailResponse = await this.cloudWalletService.getBasicMessageByConnectionId(connectionDetails); + const finalResponse: IResponse = { + statusCode: HttpStatus.OK, + message: ResponseMessages.cloudWallet.success.basicMessageByConnectionId, + data: basicMessageDetailResponse + }; + return res.status(HttpStatus.OK).json(finalResponse); + } + + /** + * Get basic-message by connection id + * @param credentialListQueryOptions + * @param res + * @returns Credential list + */ + @Post('/basic-message/:connectionId') + @ApiOperation({ summary: 'Send basic message', description: 'Send basic message' }) + @ApiResponse({ status: HttpStatus.CREATED, description: 'Created', type: ApiResponseDto }) + @UseGuards(AuthGuard('jwt'), UserRoleGuard) + async sendBasicMessage( + @Param('connectionId') connectionId: string, + @Res() res: Response, + @Body() messageDetails: BasicMessageDTO, + @User() user: user + ): Promise { + const { id, email } = user; + messageDetails.userId = id; + messageDetails.email = email; + messageDetails.connectionId = connectionId; + const basicMessageDetails = await this.cloudWalletService.sendBasicMessage(messageDetails); + const finalResponse: IResponse = { + statusCode: HttpStatus.CREATED, + message: ResponseMessages.cloudWallet.success.basicMessage, + data: basicMessageDetails + }; + return res.status(HttpStatus.CREATED).json(finalResponse); + } + +} diff --git a/apps/api-gateway/src/cloud-wallet/cloud-wallet.module.ts b/apps/api-gateway/src/cloud-wallet/cloud-wallet.module.ts new file mode 100644 index 000000000..480b0bdf8 --- /dev/null +++ b/apps/api-gateway/src/cloud-wallet/cloud-wallet.module.ts @@ -0,0 +1,24 @@ +import { getNatsOptions } from '@credebl/common/nats.config'; +import { CloudWalletController } from './cloud-wallet.controller'; +import { CloudWalletService } from './cloud-wallet.service'; +import { Module } from '@nestjs/common'; +import { ClientsModule, Transport } from '@nestjs/microservices'; +import { CommonConstants } from '@credebl/common/common.constant'; + +@Module({ + imports: [ + + ClientsModule.register([ + { + name: 'NATS_CLIENT', + transport: Transport.NATS, + options: getNatsOptions(CommonConstants.CLOUD_WALLET_SERVICE, process.env.API_GATEWAY_NKEY_SEED) + } + ]) + ], + controllers: [CloudWalletController], + providers: [CloudWalletService] +}) + +export class CloudWalletModule { +} \ No newline at end of file diff --git a/apps/api-gateway/src/cloud-wallet/cloud-wallet.service.ts b/apps/api-gateway/src/cloud-wallet/cloud-wallet.service.ts new file mode 100644 index 000000000..b7ada713f --- /dev/null +++ b/apps/api-gateway/src/cloud-wallet/cloud-wallet.service.ts @@ -0,0 +1,105 @@ + +import { IAcceptOffer, ICreateCloudWallet, ICreateCloudWalletDid, IReceiveInvitation, IAcceptProofRequest, IProofRequestRes, ICloudBaseWalletConfigure, IGetProofPresentation, IGetProofPresentationById, IGetStoredWalletInfo, IStoredWalletDetails, IWalletDetailsForDidList, IConnectionDetailsById, ITenantDetail, ICredentialDetails, ICreateConnection, IConnectionInvitationResponse, GetAllCloudWalletConnections, IBasicMessage, IBasicMessageDetails } from '@credebl/common/interfaces/cloud-wallet.interface'; +import { Inject, Injectable} from '@nestjs/common'; +import { ClientProxy } from '@nestjs/microservices'; +import { BaseService } from 'libs/service/base.service'; + +@Injectable() +export class CloudWalletService extends BaseService { + constructor(@Inject('NATS_CLIENT') private readonly cloudWalletServiceProxy: ClientProxy) { + super('CloudWalletServiceProxy'); + } + + configureBaseWallet( + cloudBaseWalletConfigure: ICloudBaseWalletConfigure + ): Promise { + return this.sendNatsMessage(this.cloudWalletServiceProxy, 'configure-cloud-base-wallet', cloudBaseWalletConfigure); + } + + createConnection( + createConnection: ICreateConnection + ): Promise { + return this.sendNatsMessage(this.cloudWalletServiceProxy, 'create-connection-by-holder', createConnection); + } + + acceptProofRequest( + acceptProofRequest: IAcceptProofRequest + ): Promise { + return this.sendNatsMessage(this.cloudWalletServiceProxy, 'accept-proof-request-by-holder', acceptProofRequest); + } + + getProofById( + proofPresentationByIdPayload: IGetProofPresentationById + ): Promise { + return this.sendNatsMessage(this.cloudWalletServiceProxy, 'get-proof-by-proof-id-holder', proofPresentationByIdPayload); + } + + getProofPresentation( + proofPresentationPayload: IGetProofPresentation + ): Promise { + return this.sendNatsMessage(this.cloudWalletServiceProxy, 'get-proof-presentation-holder', proofPresentationPayload); + } + + createCloudWallet( + cloudWalletDetails: ICreateCloudWallet + ): Promise { + return this.sendNatsMessage(this.cloudWalletServiceProxy, 'create-cloud-wallet', cloudWalletDetails); + } + + receiveInvitationByUrl( + ReceiveInvitationDetails: IReceiveInvitation + ): Promise { + return this.sendNatsMessage(this.cloudWalletServiceProxy, 'receive-invitation-by-url', ReceiveInvitationDetails); + } + + acceptOffer( + acceptOfferDetails: IAcceptOffer + ): Promise { + return this.sendNatsMessage(this.cloudWalletServiceProxy, 'accept-credential-offer', acceptOfferDetails); + } + + createDid(createDidDetails: ICreateCloudWalletDid): Promise { + return this.sendNatsMessage(this.cloudWalletServiceProxy, 'create-cloud-wallet-did', createDidDetails); +} + +getDidList( + walletDetails: IWalletDetailsForDidList +): Promise { + return this.sendNatsMessage(this.cloudWalletServiceProxy, 'cloud-wallet-did-list', walletDetails); +} + +getconnectionById( + connectionDetails: IConnectionDetailsById +): Promise { + return this.sendNatsMessage(this.cloudWalletServiceProxy, 'get-cloud-wallet-connection-by-id', connectionDetails); +} +getAllconnectionById( + connectionDetails: GetAllCloudWalletConnections +): Promise { + return this.sendNatsMessage(this.cloudWalletServiceProxy, 'get-all-cloud-wallet-connections-list-by-id', connectionDetails); +} + +getCredentialList( + tenantDetails: ITenantDetail +): Promise { + return this.sendNatsMessage(this.cloudWalletServiceProxy, 'wallet-credential-by-id', tenantDetails); +} + +getCredentialByCredentialRecordId( + credentialDetails: ICredentialDetails +): Promise { + return this.sendNatsMessage(this.cloudWalletServiceProxy, 'wallet-credential-by-record-id', credentialDetails); +} + +getBasicMessageByConnectionId( + connectionDetails: IBasicMessage +): Promise { + return this.sendNatsMessage(this.cloudWalletServiceProxy, 'basic-message-list-by-connection-id', connectionDetails); +} + +sendBasicMessage( + messageDetails: IBasicMessageDetails +): Promise { + return this.sendNatsMessage(this.cloudWalletServiceProxy, 'send-basic-message', messageDetails); +} +} diff --git a/apps/api-gateway/src/cloud-wallet/dtos/accept-proof-request.dto.ts b/apps/api-gateway/src/cloud-wallet/dtos/accept-proof-request.dto.ts new file mode 100644 index 000000000..291639217 --- /dev/null +++ b/apps/api-gateway/src/cloud-wallet/dtos/accept-proof-request.dto.ts @@ -0,0 +1,29 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IsBoolean, IsNotEmpty, IsOptional, IsString, IsUUID } from 'class-validator'; + +export class AcceptProofRequestDto { + @ApiProperty({ example: '4e687079-273b-447b-b9dd-9589c84dc6dd' }) + @IsString({ message: 'proofRecordId must be a string' }) + @IsNotEmpty({ message: 'please provide valid proofRecordId' }) + @IsUUID() + proofRecordId: string; + + @ApiPropertyOptional({ example: false }) + @IsOptional() + @IsBoolean({ message: 'filterByPresentationPreview must be a boolean' }) + filterByPresentationPreview?: boolean; + + @ApiPropertyOptional({ example: false }) + @IsOptional() + @IsBoolean({ message: 'filterByNonRevocationRequirements must be a boolean' }) + filterByNonRevocationRequirements?: boolean; + + @ApiPropertyOptional({ example: '' }) + @IsOptional() + @IsString({ message: 'comment must be a string' }) + comment?: string; + + userId: string; + + email: string; +} diff --git a/apps/api-gateway/src/cloud-wallet/dtos/cloudWallet.dto.ts b/apps/api-gateway/src/cloud-wallet/dtos/cloudWallet.dto.ts new file mode 100644 index 000000000..050eb587d --- /dev/null +++ b/apps/api-gateway/src/cloud-wallet/dtos/cloudWallet.dto.ts @@ -0,0 +1,266 @@ +import { IsBoolean, IsInt, IsNotEmpty, IsOptional, IsString, Matches, MaxLength } from 'class-validator'; + +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IsNotSQLInjection, trim } from '@credebl/common/cast.helper'; +import { Transform } from 'class-transformer'; + +export class CreateCloudWalletDto { + @ApiProperty({ example: 'Credential Wallet', description: 'Cloud wallet label' }) + @IsString({ message: 'label must be a string' }) + @IsNotEmpty({ message: 'please provide valid label' }) + @Transform(({ value }) => trim(value)) + @IsNotSQLInjection({ message: 'label is required.' }) + label: string; + + @ApiProperty({ example: 'https://picsum.photos/200', description: 'Connection image URL' }) + @IsString({ message: 'Image URL must be a string' }) + @IsOptional() + @IsNotEmpty({ message: 'please provide valid image URL' }) + @Transform(({ value }) => trim(value)) + @IsNotSQLInjection({ message: 'Image URL is required.' }) + connectionImageUrl?: string; + + email?: string; + + userId?: string; + +} + +export class ReceiveInvitationUrlDTO { + @ApiPropertyOptional() + @IsString({ message: 'alias must be a string' }) + @IsOptional() + @IsNotEmpty({ message: 'please provide valid alias' }) + @Transform(({ value }) => trim(value)) + @IsNotSQLInjection({ message: 'alias is required.' }) + alias?: string; + + @ApiPropertyOptional() + @IsString({ message: 'label must be a string' }) + @IsOptional() + @IsNotEmpty({ message: 'please provide valid label' }) + @Transform(({ value }) => trim(value)) + @IsNotSQLInjection({ message: 'label is required.' }) + label?: string; + + @ApiPropertyOptional() + @IsString({ message: 'Image URL must be a string' }) + @IsOptional() + @IsNotEmpty({ message: 'please provide valid image URL' }) + @Transform(({ value }) => trim(value)) + @IsNotSQLInjection({ message: 'Image URL is required.' }) + imageUrl?: string; + + @ApiPropertyOptional() + @IsBoolean({ message: 'autoAcceptConnection must be a boolean' }) + @Transform(({ value }) => trim(value)) + @IsOptional() + autoAcceptConnection?: boolean; + + @ApiPropertyOptional() + @IsBoolean({ message: 'autoAcceptInvitation must be a boolean' }) + @Transform(({ value }) => trim(value)) + @IsOptional() + autoAcceptInvitation?: boolean; + + @ApiPropertyOptional() + @IsBoolean({ message: 'reuseConnection must be a boolean' }) + @Transform(({ value }) => trim(value)) + @IsOptional() + reuseConnection?: boolean; + + @ApiPropertyOptional() + @IsInt({ message: 'acceptInvitationTimeoutMs must be an integer' }) + @Transform(({ value }) => trim(value)) + @IsOptional() + acceptInvitationTimeoutMs?: number; + + @ApiPropertyOptional() + @IsString({ message: 'ourDid must be a string' }) + @IsOptional() + @IsNotEmpty({ message: 'please provide valid ourDid' }) + @Transform(({ value }) => trim(value)) + @IsNotSQLInjection({ message: 'ourDid is required.' }) + ourDid?: string; + + @ApiProperty() + @IsString({ message: 'invitationUrl must be a string' }) + @IsNotEmpty({ message: 'please provide valid invitationUrl' }) + @Transform(({ value }) => trim(value)) + @IsNotSQLInjection({ message: 'invitationUrl is required.' }) + invitationUrl: string; + + email?: string; + + userId?: string; + } + + export class AcceptOfferDto { + @ApiPropertyOptional({ example: 'string', description: 'autoAcceptCredential' }) + @Transform(({ value }) => trim(value)) + @IsString({ message: 'autoAcceptCredential must be a string' }) + autoAcceptCredential: string; + + @ApiPropertyOptional({ example: 'string', description: 'Comment' }) + @Transform(({ value }) => trim(value)) + @IsString({ message: 'comment must be a string' }) + @IsOptional() + comment?: string; + + @ApiProperty({ example: 'string', description: 'Credential record ID' }) + @Transform(({ value }) => trim(value)) + @IsString({ message: 'credentialRecordId must be a string' }) + credentialRecordId: string; + + @ApiProperty({ type: Object, description: 'Credential formats' }) + credentialFormats: object; + + email?: string; + + userId?: string; + } + + export class CreateCloudWalletDidDto { + + @ApiProperty({ example: '000000000000000000000000000Seed1' }) + @MaxLength(32, { message: 'seed must be at most 32 characters.' }) + @Transform(({ value }) => trim(value)) + @IsOptional() + @ApiPropertyOptional() + @IsString({ message: 'seed must be in string format.' }) + @Matches(/^\S*$/, { + message: 'Spaces are not allowed in seed' + }) + seed?: string; + + @ApiProperty({ example: 'ed25519'}) + @IsNotEmpty({ message: 'key type is required' }) + @Transform(({ value }) => trim(value)) + @IsString({ message: 'key type be in string format.' }) + keyType: string; + + @ApiProperty({ example: 'indy'}) + @IsNotEmpty({ message: 'method is required' }) + @Transform(({ value }) => trim(value)) + @IsString({ message: 'method must be in string format.' }) + method: string; + + @ApiPropertyOptional({example: 'bcovrin:testnet'}) + @IsOptional() + @Transform(({ value }) => trim(value)) + @IsString({ message: 'network must be in string format.' }) + network?: string; + + @ApiPropertyOptional({example: 'www.github.com'}) + @IsOptional() + @Transform(({ value }) => trim(value)) + @IsString({ message: 'domain must be in string format.' }) + domain?: string; + + @ApiPropertyOptional({example: 'endorser'}) + @IsOptional() + @Transform(({ value }) => trim(value)) + @IsString({ message: 'role must be in string format.' }) + role?: string; + + @ApiPropertyOptional({example: ''}) + @IsOptional() + @IsString({ message: 'private key must be in string format.' }) + @Transform(({ value }) => trim(value)) + privatekey?: string; + + @ApiPropertyOptional({example: 'http://localhost:6006/docs'}) + @IsOptional() + @IsString({ message: 'endpoint must be in string format.' }) + endpoint?: string; + + @ApiPropertyOptional({ example: 'XzFjo1RTZ2h9UVFCnPUyaQ' }) + @IsOptional() + @Transform(({ value }) => trim(value)) + @IsString({ message: 'did must be in string format.' }) + did?: string; + + @ApiPropertyOptional({example: 'did:indy:bcovrin:testnet:UEeW111G1tYo1nEkPwMcF'}) + @IsOptional() + @Transform(({ value }) => trim(value)) + @IsString({ message: 'endorser did must be in string format.' }) + endorserDid?: string; + + email?: string; + + userId?: string; +} + +export class CredentialListDto { + @ApiProperty({ required: false}) + @IsNotEmpty() + @IsOptional() + @IsString() + threadId: string; + + @ApiProperty({ required: false}) + @IsNotEmpty() + @IsOptional() + @IsString() + connectionId: string; + + @ApiProperty({ required: false}) + @IsNotEmpty() + @IsOptional() + @IsString() + state: string; + + email?: string; + + userId?: string; +} + +export class GetAllCloudWalletConnectionsDto { + @ApiProperty({ required: false, example: 'e315f30d-9beb-4068-aea4-abb5fe5eecb1' }) + @IsNotEmpty() + @IsString() + @IsOptional() + outOfBandId: string; + + @ApiProperty({ required: false, example: 'Test' }) + @IsNotEmpty() + @IsString() + @IsOptional() + alias: string; + + @ApiProperty({ required: false, example: 'did:example:e315f30d-9beb-4068-aea4-abb5fe5eecb1' }) + @IsNotEmpty() + @IsString() + @IsOptional() + myDid: string; + + @ApiProperty({ required: false, example: 'did:example:e315f30d-9beb-4068-aea4-abb5fe5eecb1' }) + @IsNotEmpty() + @IsString() + @IsOptional() + theirDid: string; + + @ApiProperty({ required: false, example: 'Bob' }) + @IsNotEmpty() + @IsString() + @IsOptional() + theirLabel: string; + + email?: string; + + userId?: string; +} + +export class BasicMessageDTO { + @ApiProperty({ example: 'Message'}) + @IsNotEmpty({ message: 'content is required' }) + @Transform(({ value }) => trim(value)) + @IsString({ message: 'content should be in string format.' }) + content: string; + + email?: string; + + userId?: string; + + connectionId: string; +} \ No newline at end of file diff --git a/apps/api-gateway/src/cloud-wallet/dtos/configure-base-wallet.dto.ts b/apps/api-gateway/src/cloud-wallet/dtos/configure-base-wallet.dto.ts new file mode 100644 index 000000000..7c14c928a --- /dev/null +++ b/apps/api-gateway/src/cloud-wallet/dtos/configure-base-wallet.dto.ts @@ -0,0 +1,26 @@ +import { IsNotEmpty, IsString } from 'class-validator'; + +import { ApiProperty } from '@nestjs/swagger'; +import { IsHostPortOrDomain } from '@credebl/common/cast.helper'; + +export class CloudBaseWalletConfigureDto { + @ApiProperty({ example: 'xxx-xxxx-xxxx' }) + @IsString({ message: 'walletKey must be a string' }) + @IsNotEmpty({ message: 'please provide valid walletKey' }) + walletKey: string; + + @ApiProperty({ example: 'xxx-xxxx-xxxx' }) + @IsString({ message: 'apiKey must be a string' }) + @IsNotEmpty({ message: 'please provide valid apiKey' }) + apiKey: string; + + @ApiProperty({ example: 'http://0.0.0.0:4001' }) + @IsString({ message: 'agentEndpoint must be a string' }) + @IsNotEmpty({ message: 'please provide valid agentEndpoint' }) + @IsHostPortOrDomain({ message: 'Agent Endpoint must be a valid protocol://host:port or domain'}) + agentEndpoint: string; + + userId: string; + + email: string; +} diff --git a/apps/api-gateway/src/cloud-wallet/dtos/create-connection.dto.ts b/apps/api-gateway/src/cloud-wallet/dtos/create-connection.dto.ts new file mode 100644 index 000000000..2ca7742d9 --- /dev/null +++ b/apps/api-gateway/src/cloud-wallet/dtos/create-connection.dto.ts @@ -0,0 +1,212 @@ +import { + IsString, + IsBoolean, + IsArray, + IsOptional, + IsNumber, + IsObject, + ValidateNested, + IsUrl, + IsISO8601 +} from 'class-validator'; +import { Type } from 'class-transformer'; +import { ApiPropertyOptional } from '@nestjs/swagger'; + +class Thread { + @IsString() + @IsOptional() + pthid: string; + + @IsString() + @IsOptional() + thid: string; +} + +class Message { + @IsString() + @IsOptional() + '@type': string; + + @IsString() + @IsOptional() + '@id': string; + + @ValidateNested() + @IsOptional() + @Type(() => Thread) + '~thread': Thread; + + @IsString() + @IsOptional() + messageType: string; +} + +class Data { + @IsOptional() + @IsString() + base64?: string; + + @IsOptional() + @IsString() + json?: string; + + @IsOptional() + @IsArray() + @IsUrl({}, { each: true }) + links?: string[]; + + @IsOptional() + @IsObject() + jws?: { + header: object; + signature: string; + protected: string; + }; + + @IsOptional() + @IsString() + sha256?: string; +} + +class AppendedAttachment { + @IsString() + @IsOptional() + id: string; + + @IsString() + @IsOptional() + description: string; + + @IsString() + @IsOptional() + filename: string; + + @IsString() + @IsOptional() + mimeType: string; + + @IsISO8601() + @IsOptional() + lastmodTime: string; + + @IsNumber() + @IsOptional() + byteCount: number; + + @ValidateNested() + @IsOptional() + @Type(() => Data) + data: Data; +} + +export class CreateConnectionDto { + @ApiPropertyOptional() + @IsString() + @IsOptional() + label: string; + + @ApiPropertyOptional() + @IsString() + @IsOptional() + alias: string; + + @ApiPropertyOptional() + @IsString() + @IsOptional() + imageUrl: string; + + @ApiPropertyOptional({ example: true }) + @IsBoolean() + @IsOptional() + multiUseInvitation: boolean; + + @ApiPropertyOptional({ example: true }) + @IsBoolean() + @IsOptional() + autoAcceptConnection: boolean; + + @ApiPropertyOptional() + @IsString() + @IsOptional() + goalCode: string; + + @ApiPropertyOptional() + @IsString() + @IsOptional() + goal: string; + + @ApiPropertyOptional({ example: true }) + @IsBoolean() + @IsOptional() + handshake: boolean; + + @ApiPropertyOptional({ example: ['https://didcomm.org/didexchange/1.x'] }) + @IsOptional() + @IsArray() + @IsUrl({}, { each: true }) + handshakeProtocols: string[]; + + @ApiPropertyOptional({ + example: [ + { + '@type': 'string', + '@id': 'string', + '~thread': { + pthid: 'string', + thid: 'string' + }, + messageType: 'string', + additionalProp1: 'string', + additionalProp2: 'string', + additionalProp3: 'string' + } + ] + }) + @IsOptional() + @IsArray() + @ValidateNested({ each: true }) + @Type(() => Message) + messages: Message[]; + + @ApiPropertyOptional({ + example: [ + { + id: 'string', + description: 'string', + filename: 'string', + mimeType: 'string', + lastmodTime: '2024-07-19T13:24:42.255Z', + byteCount: 0, + data: { + base64: 'string', + json: 'string', + links: ['string'], + jws: { + header: {}, + signature: 'string', + protected: 'string' + }, + sha256: 'string' + } + } + ] + }) + @IsOptional() + @IsArray() + @ValidateNested({ each: true }) + @Type(() => AppendedAttachment) + appendedAttachments: AppendedAttachment[]; + + @ApiPropertyOptional() + @IsOptional() + @IsString() + invitationDid: string; + + @ApiPropertyOptional() + @IsOptional() + @IsString() + recipientKey: string; + + userId: string; + email: string; +} diff --git a/apps/api-gateway/src/cloud-wallet/enums/connections.enum.ts b/apps/api-gateway/src/cloud-wallet/enums/connections.enum.ts new file mode 100644 index 000000000..a15929cf6 --- /dev/null +++ b/apps/api-gateway/src/cloud-wallet/enums/connections.enum.ts @@ -0,0 +1,4 @@ +export declare enum HandshakeProtocol { + Connections = "https://didcomm.org/connections/1.0", + DidExchange = "https://didcomm.org/didexchange/1.0" +} \ No newline at end of file diff --git a/apps/api-gateway/src/user/dto/add-user.dto.ts b/apps/api-gateway/src/user/dto/add-user.dto.ts index 2cf2d2fce..02a37e0b4 100644 --- a/apps/api-gateway/src/user/dto/add-user.dto.ts +++ b/apps/api-gateway/src/user/dto/add-user.dto.ts @@ -1,5 +1,5 @@ import { trim } from '@credebl/common/cast.helper'; -import { ApiProperty } from '@nestjs/swagger'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { Transform } from 'class-transformer'; import { IsBoolean, IsEmail, IsNotEmpty, IsOptional, IsString, MaxLength, MinLength } from 'class-validator'; @@ -35,6 +35,11 @@ export class AddUserDetailsDto { @IsOptional() @IsBoolean({ message: 'isPasskey should be boolean' }) isPasskey?: boolean; + + @ApiPropertyOptional({ example: false }) + @IsOptional() + @IsBoolean({ message: 'isHolder should be boolean' }) + isHolder?: boolean; } export class AddPasskeyDetailsDto { diff --git a/apps/api-gateway/src/user/user.service.ts b/apps/api-gateway/src/user/user.service.ts index 168175e55..490b0678e 100644 --- a/apps/api-gateway/src/user/user.service.ts +++ b/apps/api-gateway/src/user/user.service.ts @@ -101,4 +101,9 @@ export class UserService extends BaseService { async getPlatformSettings(): Promise { return this.sendNatsMessage(this.serviceProxy, 'fetch-platform-settings', ''); } + + async getUserByUserIdInKeycloak(email: string): Promise { + const payload = { email }; + return this.sendNatsMessage(this.serviceProxy, 'get-user-info-by-user-email-keycloak', payload); + } } diff --git a/apps/cloud-wallet/src/cloud-wallet.controller.ts b/apps/cloud-wallet/src/cloud-wallet.controller.ts new file mode 100644 index 000000000..842f422d4 --- /dev/null +++ b/apps/cloud-wallet/src/cloud-wallet.controller.ts @@ -0,0 +1,92 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { Controller } from '@nestjs/common'; // Import the common service in the library +import { CloudWalletService } from './cloud-wallet.service'; // Import the common service in connection module +import { MessagePattern } from '@nestjs/microservices'; // Import the nestjs microservices package +import { IAcceptOffer, ICreateCloudWalletDid, IReceiveInvitation, IAcceptProofRequest, IProofRequestRes, ICloudBaseWalletConfigure, ICreateCloudWallet, IGetProofPresentation, IGetProofPresentationById, IGetStoredWalletInfo, IStoredWalletDetails, ICreateConnection, IConnectionInvitationResponse, IWalletDetailsForDidList, IConnectionDetailsById, ITenantDetail, ICredentialDetails, GetAllCloudWalletConnections, IBasicMessage, IBasicMessageDetails } from '@credebl/common/interfaces/cloud-wallet.interface'; + +@Controller() +export class CloudWalletController { + constructor(private readonly cloudWalletService: CloudWalletService) {} + + @MessagePattern({ cmd: 'configure-cloud-base-wallet' }) + async configureBaseWallet(configureBaseWalletPayload: ICloudBaseWalletConfigure): Promise { + return this.cloudWalletService.configureBaseWallet(configureBaseWalletPayload); + } + + @MessagePattern({ cmd: 'create-connection-by-holder' }) + async createConnection(createConnection: ICreateConnection): Promise { + return this.cloudWalletService.createConnection(createConnection); + } + + @MessagePattern({ cmd: 'accept-proof-request-by-holder' }) + async acceptProofRequest(acceptProofRequestPayload: IAcceptProofRequest): Promise { + return this.cloudWalletService.acceptProofRequest(acceptProofRequestPayload); + } + + @MessagePattern({ cmd: 'get-proof-by-proof-id-holder' }) + async getProofById(proofPrsentationByIdPayload: IGetProofPresentationById): Promise { + return this.cloudWalletService.getProofById(proofPrsentationByIdPayload); + } + + @MessagePattern({ cmd: 'get-proof-presentation-holder' }) + async getProofPresentation(proofPresentationPayload: IGetProofPresentation): Promise { + return this.cloudWalletService.getProofPresentation(proofPresentationPayload); + } + + @MessagePattern({ cmd: 'create-cloud-wallet' }) + async createConnectionInvitation(cloudWalletDetails: ICreateCloudWallet): Promise { + return this.cloudWalletService.createCloudWallet(cloudWalletDetails); + } + + @MessagePattern({ cmd: 'receive-invitation-by-url' }) + async receiveInvitationByUrl(ReceiveInvitationDetails: IReceiveInvitation): Promise { + return this.cloudWalletService.receiveInvitationByUrl(ReceiveInvitationDetails); + } + + @MessagePattern({ cmd: 'accept-credential-offer' }) + async acceptOffer(acceptOfferDetails: IAcceptOffer): Promise { + return this.cloudWalletService.acceptOffer(acceptOfferDetails); + } + + @MessagePattern({ cmd: 'create-cloud-wallet-did' }) + async createDid(createDidDetails: ICreateCloudWalletDid): Promise { + return this.cloudWalletService.createDid(createDidDetails); + } + + @MessagePattern({ cmd: 'cloud-wallet-did-list' }) + async getDidList(walletDetails: IWalletDetailsForDidList): Promise { + return this.cloudWalletService.getDidList(walletDetails); + } + + @MessagePattern({ cmd: 'get-cloud-wallet-connection-by-id' }) + async getconnectionById(connectionDetails: IConnectionDetailsById): Promise { + return this.cloudWalletService.getconnectionById(connectionDetails); + } + + + @MessagePattern({ cmd: 'get-all-cloud-wallet-connections-list-by-id' }) + async getAllconnectionById(connectionDetails: GetAllCloudWalletConnections): Promise { + return this.cloudWalletService.getAllconnectionById(connectionDetails); + } + + @MessagePattern({ cmd: 'wallet-credential-by-id' }) + async getCredentialList(tenantDetails: ITenantDetail): Promise { + return this.cloudWalletService.getCredentialListById(tenantDetails); + } + + @MessagePattern({ cmd: 'wallet-credential-by-record-id' }) + async getCredentialByCredentialRecordId(credentialDetails: ICredentialDetails): Promise { + return this.cloudWalletService.getCredentialByRecord(credentialDetails); + } + + @MessagePattern({ cmd: 'basic-message-list-by-connection-id' }) + async getBasicMessageByConnectionId(connectionDetails: IBasicMessage): Promise { + return this.cloudWalletService.getBasicMessageByConnectionId(connectionDetails); + } + + @MessagePattern({ cmd: 'send-basic-message' }) + async sendBasicMessage(messageDetails: IBasicMessageDetails): Promise { + return this.cloudWalletService.sendBasicMessage(messageDetails); + } + +} \ No newline at end of file diff --git a/apps/cloud-wallet/src/cloud-wallet.module.ts b/apps/cloud-wallet/src/cloud-wallet.module.ts new file mode 100644 index 000000000..895aac9a2 --- /dev/null +++ b/apps/cloud-wallet/src/cloud-wallet.module.ts @@ -0,0 +1,27 @@ +import { Logger, Module } from '@nestjs/common'; +import { CloudWalletController } from './cloud-wallet.controller'; +import { CloudWalletService } from './cloud-wallet.service'; +import { ClientsModule, Transport } from '@nestjs/microservices'; +import { CommonModule } from '@credebl/common'; +import { CacheModule } from '@nestjs/cache-manager'; +import { getNatsOptions } from '@credebl/common/nats.config'; +import { PrismaService } from '@credebl/prisma-service'; +import { CloudWalletRepository } from './cloud-wallet.repository'; + +@Module({ + imports: [ +ClientsModule.register([ + { + name: 'NATS_CLIENT', + transport: Transport.NATS, + options: getNatsOptions(process.env.CLOUD_WALLET_NKEY_SEED) + } + ]), + + CommonModule, + CacheModule.register() +], + controllers: [CloudWalletController], + providers: [CloudWalletService, CloudWalletRepository, PrismaService, Logger] +}) +export class CloudWalletModule {} diff --git a/apps/cloud-wallet/src/cloud-wallet.repository.ts b/apps/cloud-wallet/src/cloud-wallet.repository.ts new file mode 100644 index 000000000..cbf63f788 --- /dev/null +++ b/apps/cloud-wallet/src/cloud-wallet.repository.ts @@ -0,0 +1,154 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { PrismaService } from '@credebl/prisma-service'; +import { CloudWalletType } from '@credebl/enum/enum'; +// eslint-disable-next-line camelcase +import { cloud_wallet_user_info, user } from '@prisma/client'; +import { ICloudWalletDetails, IGetStoredWalletInfo, IStoredWalletDetails, IStoreWalletInfo } from '@credebl/common/interfaces/cloud-wallet.interface'; + + +@Injectable() +export class CloudWalletRepository { + constructor( + private readonly prisma: PrismaService, + private readonly logger: Logger + ) {} + + + // eslint-disable-next-line camelcase + async getCloudWalletDetails(type: CloudWalletType): Promise { + try { + const agentDetails = await this.prisma.cloud_wallet_user_info.findFirstOrThrow({ + where: { + type + } + }); + return agentDetails; + } catch (error) { + this.logger.error(`Error in getCloudWalletBaseAgentDetails: ${error.message}`); + throw error; + } + } + + // eslint-disable-next-line camelcase + async checkUserExist(email: string): Promise { + try { + const agentDetails = await this.prisma.cloud_wallet_user_info.findUnique({ + where: { + email + } + }); + return agentDetails; + } catch (error) { + this.logger.error(`Error in getCloudWalletBaseAgentDetails: ${error.message}`); + throw error; + } + } + // eslint-disable-next-line camelcase + async storeCloudWalletDetails(cloudWalletDetails: ICloudWalletDetails): Promise { + try { + const {label, lastChangedBy, tenantId, type, userId, agentApiKey, agentEndpoint, email, key, connectionImageUrl} = cloudWalletDetails; + + return await this.prisma.cloud_wallet_user_info.create({ + data: { + label, + tenantId, + email, + type, + createdBy: userId, + lastChangedBy, + userId, + agentEndpoint, + agentApiKey, + key, + connectionImageUrl + }, + select: { + email: true, + connectionImageUrl: true, + createDateTime: true, + id: true, + tenantId: true, + label: true, + lastChangedDateTime: true + + } + }); + } catch (error) { + this.logger.error(`Error in storeCloudWalletDetails: ${error.message}`); + throw error; + } + } + + // eslint-disable-next-line camelcase + async getCloudWalletInfo(email: string): Promise { + try { + const walletInfoData = await this.prisma.cloud_wallet_user_info.findUnique({ + where: { + email + } + }); + return walletInfoData; + } catch (error) { + this.logger.error(`Error in getCloudWalletInfo: ${error}`); + throw error; + } + } + + async storeCloudWalletInfo(cloudWalletInfoPayload: IStoreWalletInfo): Promise { + try { + const { agentEndpoint, agentApiKey, email, type, userId, key, createdBy, lastChangedBy } = cloudWalletInfoPayload; + const walletInfoData = await this.prisma.cloud_wallet_user_info.create({ + data: { + type, + agentApiKey, + agentEndpoint, + email, + userId, + key, + createdBy, + lastChangedBy + }, + select: { + id: true, + email: true, + type: true, + userId: true, + agentEndpoint: true + } + }); + return walletInfoData; + } catch (error) { + this.logger.error(`Error in storeCloudWalletInfo: ${error}`); + throw error; + } + } + + // eslint-disable-next-line camelcase + async getCloudSubWallet(userId: string): Promise { + try { + const cloudSubWalletDetails = await this.prisma.cloud_wallet_user_info.findFirstOrThrow({ + where: { + userId + } + }); + return cloudSubWalletDetails; + } catch (error) { + this.logger.error(`Error in getCloudSubWallet: ${error}`); + throw error; + } + } + + async getUserInfo(email: string): Promise { + try { + const userDetails = await this.prisma.user.findUnique({ + where: { + email + } + }); + return userDetails; + } catch (error) { + this.logger.error(`Error in getUserInfo: ${error}`); + throw error; + } + } +} diff --git a/apps/cloud-wallet/src/cloud-wallet.service.ts b/apps/cloud-wallet/src/cloud-wallet.service.ts new file mode 100644 index 000000000..5dcd378c1 --- /dev/null +++ b/apps/cloud-wallet/src/cloud-wallet.service.ts @@ -0,0 +1,618 @@ +/* eslint-disable camelcase */ +import { CommonService } from '@credebl/common'; +import { + BadRequestException, + ConflictException, + Inject, + Injectable, + InternalServerErrorException, + Logger, + NotFoundException +} from '@nestjs/common'; +import { ClientProxy } from '@nestjs/microservices'; +import { Cache } from 'cache-manager'; +import { CACHE_MANAGER } from '@nestjs/cache-manager'; +import { + IAcceptOffer, + ICreateCloudWalletDid, + IReceiveInvitation, + IAcceptProofRequest, + IProofRequestRes, + ICloudBaseWalletConfigure, + ICloudWalletDetails, + ICreateCloudWallet, + IGetProofPresentation, + IGetProofPresentationById, + IGetStoredWalletInfo, + IStoredWalletDetails, + CloudWallet, + IStoreWalletInfo, + IWalletDetailsForDidList, + IConnectionDetailsById, + ITenantDetail, + ICredentialDetails, + ICreateConnection, + IConnectionInvitationResponse, + GetAllCloudWalletConnections, + IBasicMessage, + IBasicMessageDetails +} from '@credebl/common/interfaces/cloud-wallet.interface'; +import { CloudWalletRepository } from './cloud-wallet.repository'; +import { ResponseMessages } from '@credebl/common/response-messages'; +import { CloudWalletType } from '@credebl/enum/enum'; +import { CommonConstants } from '@credebl/common/common.constant'; + +@Injectable() +export class CloudWalletService { + constructor( + private readonly commonService: CommonService, + @Inject('NATS_CLIENT') private readonly cloudWalletServiceProxy: ClientProxy, + private readonly cloudWalletRepository: CloudWalletRepository, + private readonly logger: Logger, + @Inject(CACHE_MANAGER) private cacheService: Cache + ) {} + + /** + * configure cloud base wallet + * @param configureBaseWalletPayload + * @returns cloud base wallet + */ + async configureBaseWallet(configureBaseWalletPayload: ICloudBaseWalletConfigure): Promise { + const { agentEndpoint, apiKey, email, walletKey, userId } = configureBaseWalletPayload; + + try { + const getAgentInfo = await this.commonService.httpGet( + `${agentEndpoint}${CommonConstants.URL_AGENT_GET_ENDPOINT}` + ); + if (!getAgentInfo?.isInitialized) { + throw new BadRequestException(ResponseMessages.cloudWallet.error.notReachable); + } + + const existingWalletInfo = await this.cloudWalletRepository.getCloudWalletInfo(email); + if (existingWalletInfo) { + throw new ConflictException(ResponseMessages.cloudWallet.error.agentAlreadyExist); + } + + const [encryptionWalletKey, encryptionApiKey] = await Promise.all([ + this.commonService.dataEncryption(walletKey), + this.commonService.dataEncryption(apiKey) + ]); + + const walletInfoToStore: IStoreWalletInfo = { + agentEndpoint, + agentApiKey: encryptionApiKey, + email, + type: CloudWalletType.BASE_WALLET, + userId, + key: encryptionWalletKey, + createdBy: userId, + lastChangedBy: userId + }; + + const storedWalletInfo = await this.cloudWalletRepository.storeCloudWalletInfo(walletInfoToStore); + return storedWalletInfo; + } catch (error) { + await this.commonService.handleError(error); + throw error; + } + } + + /** + * Create connection + * @param createConnection + * @returns connection details + */ + async createConnection(createConnection: ICreateConnection): Promise { + try { + + const { userId, ...connectionPayload } = createConnection; + const [baseWalletDetails, getTenant, decryptedApiKey] = await this._commonCloudWalletInfo(userId); + + delete connectionPayload.email; + + const { tenantId } = getTenant; + const { agentEndpoint } = baseWalletDetails; + + const url = `${agentEndpoint}${CommonConstants.CLOUD_WALLET_CREATE_CONNECTION_INVITATION}/${tenantId}`; + + const createConnectionDetails = await this.commonService.httpPost(url, connectionPayload, { headers: { authorization: decryptedApiKey } }); + return createConnectionDetails; + } catch (error) { + await this.commonService.handleError(error); + throw error; + } + } + + /** + * Accept proof request + * @param acceptProofRequest + * @returns proof presentation + */ + async acceptProofRequest(acceptProofRequest: IAcceptProofRequest): Promise { + const { proofRecordId, comment, filterByNonRevocationRequirements, filterByPresentationPreview, userId } = + acceptProofRequest; + try { + const [baseWalletDetails, getTenant, decryptedApiKey] = await this._commonCloudWalletInfo(userId); + + const { tenantId } = getTenant; + const { agentEndpoint } = baseWalletDetails; + + const url = `${agentEndpoint}${CommonConstants.CLOUD_WALLET_GET_PROOF_REQUEST}/${proofRecordId}${CommonConstants.CLOUD_WALLET_ACCEPT_PROOF_REQUEST}${tenantId}`; + const proofAcceptRequestPayload = { + comment, + filterByNonRevocationRequirements, + filterByPresentationPreview + }; + + const acceptProofRequest = await this.commonService.httpPost(url, proofAcceptRequestPayload, { + headers: { authorization: decryptedApiKey } + }); + return acceptProofRequest; + } catch (error) { + await this.commonService.handleError(error); + throw error; + } + } + + /** + * Get proof presentation by proof Id + * @param proofPrsentationByIdPayload + * @returns proof presentation + */ + async getProofById(proofPrsentationByIdPayload: IGetProofPresentationById): Promise { + try { + const { proofRecordId, userId } = proofPrsentationByIdPayload; + const [baseWalletDetails, getTenant, decryptedApiKey] = await this._commonCloudWalletInfo(userId); + + const { tenantId } = getTenant; + const { agentEndpoint } = baseWalletDetails; + + const url = `${agentEndpoint}${CommonConstants.CLOUD_WALLET_GET_PROOF_REQUEST}/${proofRecordId}/${tenantId}`; + + const getProofById = await this.commonService.httpGet(url, { headers: { authorization: decryptedApiKey } }); + return getProofById; + } catch (error) { + await this.commonService.handleError(error); + throw error; + } + } + + /** + * Get proof presentation + * @param proofPresentationPayload + * @returns proof presentations + */ + async getProofPresentation(proofPresentationPayload: IGetProofPresentation): Promise { + try { + const { threadId, userId } = proofPresentationPayload; + + const [baseWalletDetails, getTenant, decryptedApiKey] = await this._commonCloudWalletInfo(userId); + + const { tenantId } = getTenant; + const { agentEndpoint } = baseWalletDetails; + + const url = `${agentEndpoint}${CommonConstants.CLOUD_WALLET_GET_PROOF_REQUEST}/${tenantId}${ + threadId ? `?threadId=${threadId}` : '' + }`; + + const getProofById = await this.commonService.httpGet(url, { headers: { authorization: decryptedApiKey } }); + return getProofById; + } catch (error) { + await this.commonService.handleError(error); + throw error; + } + } + + /** + * common function for get cloud wallet + * @param userId + * @returns cloud wallet info + */ + async _commonCloudWalletInfo(userId: string): Promise<[CloudWallet, CloudWallet, string]> { + const baseWalletDetails = await this.cloudWalletRepository.getCloudWalletDetails(CloudWalletType.BASE_WALLET); + + if (!baseWalletDetails) { + throw new NotFoundException(ResponseMessages.cloudWallet.error.notFoundBaseWallet); + } + + const getAgentDetails = await this.commonService.httpGet( + `${baseWalletDetails?.agentEndpoint}${CommonConstants.URL_AGENT_GET_ENDPOINT}` + ); + if (!getAgentDetails?.isInitialized) { + throw new BadRequestException(ResponseMessages.cloudWallet.error.notReachable); + } + + const getTenant = await this.cloudWalletRepository.getCloudSubWallet(userId); + + if (!getTenant || !getTenant?.tenantId) { + throw new NotFoundException(ResponseMessages.cloudWallet.error.walletRecordNotFound); + } + + const decryptedApiKey = await this.commonService.decryptPassword(getTenant?.agentApiKey); + + return [baseWalletDetails, getTenant, decryptedApiKey]; + } + + /** + * Create clous wallet + * @param cloudWalletDetails + * @returns cloud wallet details + */ + async createCloudWallet(cloudWalletDetails: ICreateCloudWallet): Promise { + try { + const { label, connectionImageUrl, email, userId } = cloudWalletDetails; + const agentPayload = { + config: { + label, + connectionImageUrl + } + }; + + const checkUserExist = await this.cloudWalletRepository.checkUserExist(email); + + if (checkUserExist) { + throw new ConflictException(ResponseMessages.cloudWallet.error.userExist); + } + + const baseWalletDetails = await this.cloudWalletRepository.getCloudWalletDetails(CloudWalletType.BASE_WALLET); + + const { agentEndpoint, agentApiKey } = baseWalletDetails; + const url = `${agentEndpoint}${CommonConstants.URL_SHAGENT_CREATE_TENANT}`; + const decryptedApiKey = await this.commonService.decryptPassword(agentApiKey); + + const checkCloudWalletAgentHealth = await this.commonService.checkAgentHealth(agentEndpoint, decryptedApiKey); + + if (!checkCloudWalletAgentHealth) { + throw new NotFoundException(ResponseMessages.cloudWallet.error.agentNotRunning); + } + const createCloudWalletResponse = await this.commonService.httpPost(url, agentPayload, { + headers: { authorization: decryptedApiKey } + }); + + if (!createCloudWalletResponse && !createCloudWalletResponse.id) { + throw new InternalServerErrorException(ResponseMessages.cloudWallet.error.createCloudWallet, { + cause: new Error(), + description: ResponseMessages.errorMessages.serverError + }); + } + + const walletKey = await this.commonService.dataEncryption(createCloudWalletResponse.config.walletConfig.key); + + if (!walletKey) { + throw new BadRequestException(ResponseMessages.cloudWallet.error.encryptCloudWalletKey, { + cause: new Error(), + description: ResponseMessages.errorMessages.serverError + }); + } + + const cloudWalletResponse: ICloudWalletDetails = { + createdBy: userId, + label, + lastChangedBy: userId, + tenantId: createCloudWalletResponse.id, + type: CloudWalletType.SUB_WALLET, + userId, + agentApiKey, + agentEndpoint, + email, + key: walletKey, + connectionImageUrl + }; + const storeCloudWalletDetails = await this.cloudWalletRepository.storeCloudWalletDetails(cloudWalletResponse); + return storeCloudWalletDetails; + } catch (error) { + this.logger.error(`[createCloudWallet] - error in create cloud wallet: ${error}`); + await this.commonService.handleError(error); + } + } + + /** + * Receive invitation + * @param ReceiveInvitationDetails + * @returns Invitation details + */ + async receiveInvitationByUrl(ReceiveInvitationDetails: IReceiveInvitation): Promise { + try { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { email, userId, ...invitationDetails } = ReceiveInvitationDetails; + + const checkUserExist = await this.cloudWalletRepository.checkUserExist(email); + + if (!checkUserExist) { + throw new ConflictException(ResponseMessages.cloudWallet.error.walletNotExist); + } + + const [baseWalletDetails, getTenant, decryptedApiKey] = await this._commonCloudWalletInfo(userId); + + const { tenantId } = getTenant; + const { agentEndpoint } = baseWalletDetails; + const url = `${agentEndpoint}${CommonConstants.RECEIVE_INVITATION_BY_URL}${tenantId}`; + + const checkCloudWalletAgentHealth = await this.commonService.checkAgentHealth(agentEndpoint, decryptedApiKey); + + if (!checkCloudWalletAgentHealth) { + throw new NotFoundException(ResponseMessages.cloudWallet.error.agentNotRunning); + } + const receiveInvitationResponse = await this.commonService.httpPost(url, invitationDetails, { + headers: { authorization: decryptedApiKey } + }); + + if (!receiveInvitationResponse) { + throw new InternalServerErrorException(ResponseMessages.cloudWallet.error.receiveInvitation, { + cause: new Error(), + description: ResponseMessages.errorMessages.serverError + }); + } + + return receiveInvitationResponse; + } catch (error) { + this.logger.error(`[createCloudWallet] - error in receive invitation: ${error}`); + await this.commonService.handleError(error); + } + } + + /** + * Accept offer + * @param acceptOfferDetails + * @returns Offer details + */ + async acceptOffer(acceptOfferDetails: IAcceptOffer): Promise { + try { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { email, userId, ...offerDetails } = acceptOfferDetails; + + const checkUserExist = await this.cloudWalletRepository.checkUserExist(email); + + if (!checkUserExist) { + throw new ConflictException(ResponseMessages.cloudWallet.error.walletNotExist); + } + const [baseWalletDetails, getTenant, decryptedApiKey] = await this._commonCloudWalletInfo(userId); + + const { tenantId } = getTenant; + const { agentEndpoint } = baseWalletDetails; + + const url = `${agentEndpoint}${CommonConstants.ACCEPT_OFFER}${tenantId}`; + + const checkCloudWalletAgentHealth = await this.commonService.checkAgentHealth(agentEndpoint, decryptedApiKey); + + if (!checkCloudWalletAgentHealth) { + throw new NotFoundException(ResponseMessages.cloudWallet.error.agentNotRunning); + } + const acceptOfferResponse = await this.commonService.httpPost(url, offerDetails, { + headers: { authorization: decryptedApiKey } + }); + + if (!acceptOfferResponse) { + throw new InternalServerErrorException(ResponseMessages.cloudWallet.error.receiveInvitation, { + cause: new Error(), + description: ResponseMessages.errorMessages.serverError + }); + } + + return acceptOfferResponse; + } catch (error) { + this.logger.error(`[receiveInvitationByUrl] - error in accept offer: ${error}`); + await this.commonService.handleError(error); + } + } + + /** + * Create DID for cloud wallet + * @param createDidDetails + * @returns DID details + */ + async createDid(createDidDetails: ICreateCloudWalletDid): Promise { + try { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { email, userId, ...didDetails } = createDidDetails; + + const checkUserExist = await this.cloudWalletRepository.checkUserExist(email); + + if (!checkUserExist) { + throw new ConflictException(ResponseMessages.cloudWallet.error.walletNotExist); + } + const [baseWalletDetails, getTenant, decryptedApiKey] = await this._commonCloudWalletInfo(userId); + + const { tenantId } = getTenant; + const { agentEndpoint } = baseWalletDetails; + + const url = `${agentEndpoint}${CommonConstants.URL_SHAGENT_CREATE_DID}${tenantId}`; + + const checkCloudWalletAgentHealth = await this.commonService.checkAgentHealth(agentEndpoint, decryptedApiKey); + + if (!checkCloudWalletAgentHealth) { + throw new NotFoundException(ResponseMessages.cloudWallet.error.agentNotRunning); + } + const didDetailsResponse = await this.commonService.httpPost(url, didDetails, { + headers: { authorization: decryptedApiKey } + }); + + if (!didDetailsResponse) { + throw new InternalServerErrorException(ResponseMessages.cloudWallet.error.receiveInvitation, { + cause: new Error(), + description: ResponseMessages.errorMessages.serverError + }); + } + + return didDetailsResponse; + } catch (error) { + this.logger.error(`[createDid] - error in create DID: ${error}`); + await this.commonService.handleError(error); + } + } + + /** + * Get DID list by tenant id + * @param walletDetails + * @returns DID list + */ + async getDidList(walletDetails: IWalletDetailsForDidList): Promise { + try { + const { userId } = walletDetails; + const [baseWalletDetails, getTenant, decryptedApiKey] = await this._commonCloudWalletInfo(userId); + + const { tenantId } = getTenant; + const { agentEndpoint } = baseWalletDetails; + + const url = `${agentEndpoint}${CommonConstants.CLOUD_WALLET_DID_LIST}${tenantId}`; + + const didList = await this.commonService.httpGet(url, { headers: { authorization: decryptedApiKey } }); + return didList; + } catch (error) { + await this.commonService.handleError(error); + throw error; + } + } + + /** + * Get connection details by tenant id and connection id + * @param connectionDetails + * @returns Connection Details + */ + async getconnectionById(connectionDetails: IConnectionDetailsById): Promise { + try { + const { userId, connectionId } = connectionDetails; + const [baseWalletDetails, getTenant, decryptedApiKey] = await this._commonCloudWalletInfo(userId); + + const { tenantId } = getTenant; + const { agentEndpoint } = baseWalletDetails; + + const url = `${agentEndpoint}${CommonConstants.CLOUD_WALLET_CONNECTION_BY_ID}${connectionId}/${tenantId}`; + + const connectionDetailResponse = await this.commonService.httpGet(url, { headers: { authorization: decryptedApiKey } }); + return connectionDetailResponse; + } catch (error) { + await this.commonService.handleError(error); + throw error; + } + } + + /** + * Get connection list by tenant id + * @param connectionDetails + * @returns Connection Details + */ + async getAllconnectionById(connectionDetails: GetAllCloudWalletConnections): Promise { + try { + const { userId, alias, myDid, outOfBandId, theirDid, theirLabel } = connectionDetails; + const [baseWalletDetails, getTenant, decryptedApiKey] = await this._commonCloudWalletInfo(userId); + const urlOptions = { + alias, + myDid, + outOfBandId, + theirDid, + theirLabel + }; + const optionalParameter = await this.commonService.createDynamicUrl(urlOptions); + const { tenantId } = getTenant; + const { agentEndpoint } = baseWalletDetails; + + const url = `${agentEndpoint}${CommonConstants.CLOUD_WALLET_CONNECTION_BY_ID}${tenantId}${optionalParameter}`; + + const connectionDetailList = await this.commonService.httpGet(url, { headers: { authorization: decryptedApiKey } }); + return connectionDetailList; + } catch (error) { + await this.commonService.handleError(error); + throw error; + } + } + + /** + * Get credential list by tenant id + * @param tenantDetails + * @returns Connection Details + */ + async getCredentialListById(tenantDetails: ITenantDetail): Promise { + try { + const { userId, connectionId, state, threadId } = tenantDetails; + const [baseWalletDetails, getTenant, decryptedApiKey] = await this._commonCloudWalletInfo(userId); + const urlOptions = { + connectionId, + state, + threadId + }; + const {tenantId} = getTenant; + const optionalParameter = await this.commonService.createDynamicUrl(urlOptions); + + const { agentEndpoint } = baseWalletDetails; + + const url = `${agentEndpoint}${CommonConstants.CLOUD_WALLET_CREDENTIAL}/${tenantId}${optionalParameter}`; + + const credentialDetailResponse = await this.commonService.httpGet(url, { headers: { authorization: decryptedApiKey } }); + return credentialDetailResponse; + } catch (error) { + await this.commonService.handleError(error); + throw error; + } + } + + /** + * Get credential by record id + * @param credentialDetails + * @returns Connection Details + */ + async getCredentialByRecord(credentialDetails: ICredentialDetails): Promise { + try { + const { userId, credentialRecordId } = credentialDetails; + const [baseWalletDetails, getTenant, decryptedApiKey] = await this._commonCloudWalletInfo(userId); + + const {tenantId} = getTenant; + const { agentEndpoint } = baseWalletDetails; + + const url = `${agentEndpoint}${CommonConstants.CLOUD_WALLET_CREDENTIAL}/${credentialRecordId}${tenantId}`; + + const credentialDetailResponse = await this.commonService.httpGet(url, { headers: { authorization: decryptedApiKey } }); + return credentialDetailResponse; + } catch (error) { + await this.commonService.handleError(error); + throw error; + } + } + + /** + * Get basic-message by connection id + * @param connectionDetails + * @returns Basic message Details + */ + async getBasicMessageByConnectionId(connectionDetails: IBasicMessage): Promise { + try { + const { userId, connectionId } = connectionDetails; + const [baseWalletDetails, getTenant, decryptedApiKey] = await this._commonCloudWalletInfo(userId); + + const {tenantId} = getTenant; + const { agentEndpoint } = baseWalletDetails; + + const url = `${agentEndpoint}${CommonConstants.CLOUD_WALLET_BASIC_MESSAGE}${connectionId}/${tenantId}`; + + const basicMessageResponse = await this.commonService.httpGet(url, { headers: { authorization: decryptedApiKey } }); + return basicMessageResponse; + } catch (error) { + await this.commonService.handleError(error); + throw error; + } + } + + /** + * Send basic-message by connection id + * @param messageDetails + * @returns Basic message Details + */ + async sendBasicMessage(messageDetails: IBasicMessageDetails): Promise { + try { + const { userId, connectionId, content } = messageDetails; + const [baseWalletDetails, getTenant, decryptedApiKey] = await this._commonCloudWalletInfo(userId); + + const {tenantId} = getTenant; + const { agentEndpoint } = baseWalletDetails; + + const url = `${agentEndpoint}${CommonConstants.CLOUD_WALLET_BASIC_MESSAGE}${connectionId}/${tenantId}`; + const basicMessageResponse = await this.commonService.httpPost(url, {content}, { + headers: { authorization: decryptedApiKey } + }); + return basicMessageResponse; + } catch (error) { + await this.commonService.handleError(error); + throw error; + } + } +} diff --git a/apps/cloud-wallet/src/main.ts b/apps/cloud-wallet/src/main.ts new file mode 100644 index 000000000..2168ad546 --- /dev/null +++ b/apps/cloud-wallet/src/main.ts @@ -0,0 +1,23 @@ +import { NestFactory } from '@nestjs/core'; +import { CloudWalletModule } from './cloud-wallet.module'; +import { HttpExceptionFilter } from 'libs/http-exception.filter'; +import { Logger } from '@nestjs/common'; +import { MicroserviceOptions, Transport } from '@nestjs/microservices'; +import { getNatsOptions } from '@credebl/common/nats.config'; +import { CommonConstants } from '@credebl/common/common.constant'; + +const logger = new Logger(); + +async function bootstrap(): Promise { + + const app = await NestFactory.createMicroservice(CloudWalletModule, { + transport: Transport.NATS, + options: getNatsOptions(CommonConstants.CLOUD_WALLET_SERVICE, process.env.CLOUD_WALLET_NKEY_SEED) + }); + + app.useGlobalFilters(new HttpExceptionFilter()); + + await app.listen(); + logger.log('Cloud-wallet Microservice is listening to NATS '); +} +bootstrap(); diff --git a/apps/cloud-wallet/test/jest-e2e.json b/apps/cloud-wallet/test/jest-e2e.json new file mode 100644 index 000000000..e9d912f3e --- /dev/null +++ b/apps/cloud-wallet/test/jest-e2e.json @@ -0,0 +1,9 @@ +{ + "moduleFileExtensions": ["js", "json", "ts"], + "rootDir": ".", + "testEnvironment": "node", + "testRegex": ".e2e-spec.ts$", + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + } +} diff --git a/apps/cloud-wallet/tsconfig.app.json b/apps/cloud-wallet/tsconfig.app.json new file mode 100644 index 000000000..0788dd2e8 --- /dev/null +++ b/apps/cloud-wallet/tsconfig.app.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "declaration": false, + "outDir": "../../dist/apps/cloud-wallet" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] +} diff --git a/apps/user/interfaces/user.interface.ts b/apps/user/interfaces/user.interface.ts index 533b4c1a5..4db0a736d 100644 --- a/apps/user/interfaces/user.interface.ts +++ b/apps/user/interfaces/user.interface.ts @@ -1,4 +1,4 @@ -import { Prisma, RecordType } from '@prisma/client'; +import { $Enums, Prisma, RecordType } from '@prisma/client'; export interface IUsersProfile { id: string; @@ -61,6 +61,7 @@ export interface IUserInformation { firstName: string; lastName: string; isPasskey: boolean; + isHolder?: boolean; } export interface AddPasskeyDetails { @@ -222,4 +223,15 @@ export interface UserKeycloakId { id: string; keycloakUserId: string; email: string; +} + +export interface UserRoleMapping { + id: string; + userId: string; + userRoleId: string; +} + +export interface UserRoleDetails{ + id: string; + role: $Enums.UserRole; } \ No newline at end of file diff --git a/apps/user/repositories/user.repository.ts b/apps/user/repositories/user.repository.ts index 69a8f0b6a..2f02bd86d 100644 --- a/apps/user/repositories/user.repository.ts +++ b/apps/user/repositories/user.repository.ts @@ -12,12 +12,15 @@ import { IUserInformation, IVerifyUserEmail, IUserDeletedActivity, - UserKeycloakId + UserKeycloakId, + UserRoleMapping, + UserRoleDetails } from '../interfaces/user.interface'; import { InternalServerErrorException } from '@nestjs/common'; import { PrismaService } from '@credebl/prisma-service'; // eslint-disable-next-line camelcase import { RecordType, schema, token, user } from '@prisma/client'; +import { UserRole } from '@credebl/enum/enum'; interface UserQueryOptions { id?: string; // Use the appropriate type based on your data model @@ -836,4 +839,32 @@ export class UserRepository { } } + async storeUserRole(userId: string, userRoleId: string): Promise { + try { + const userRoleMapping = await this.prisma.user_role_mapping.create({ + data: { + userId, + userRoleId + } + }); + return userRoleMapping; + } catch (error) { + this.logger.error(`Error in storeUserRole: ${error.message} `); + throw error; + } + } + + async getUserRole(role: UserRole): Promise { + try { + const getUserRole = await this.prisma.user_role.findFirstOrThrow({ + where: { + role + } + }); + return getUserRole; + } catch (error) { + this.logger.error(`Error in getUserRole: ${error.message} `); + throw error; + } + } } diff --git a/apps/user/src/user.controller.ts b/apps/user/src/user.controller.ts index 9d64b2895..466ca5b6c 100644 --- a/apps/user/src/user.controller.ts +++ b/apps/user/src/user.controller.ts @@ -220,4 +220,9 @@ export class UserController { return this.userService.getUserKeycloakIdByEmail(userEmails); } + @MessagePattern({ cmd: 'get-user-info-by-user-email-keycloak' }) + async getUserByUserIdInKeycloak(payload: {email}): Promise { + return this.userService.getUserByUserIdInKeycloak(payload.email); + } + } diff --git a/apps/user/src/user.service.ts b/apps/user/src/user.service.ts index b4d0259c1..43934c1a5 100644 --- a/apps/user/src/user.service.ts +++ b/apps/user/src/user.service.ts @@ -49,7 +49,7 @@ 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, Invitation, UserCertificateId } from '@credebl/enum/enum'; +import { EcosystemConfigSettings, Invitation, UserCertificateId, UserRole } from '@credebl/enum/enum'; import { WinnerTemplate } from '../templates/winner-template'; import { ParticipantTemplate } from '../templates/participant-template'; import { ArbiterTemplate } from '../templates/arbiter-template'; @@ -252,10 +252,9 @@ export class UserService { if (!userDetails) { throw new NotFoundException(ResponseMessages.user.error.adduser); } - - let keycloakDetails = null; - - const token = await this.clientRegistrationService.getManagementToken(checkUserDetails.clientId, checkUserDetails.clientSecret); + let keycloakDetails = null; + + const token = await this.clientRegistrationService.getManagementToken(checkUserDetails.clientId, checkUserDetails.clientSecret); if (userInfo.isPasskey) { const resUser = await this.userRepository.addUserPassword(email.toLowerCase(), userInfo.password); const userDetails = await this.userRepository.getUserDetails(email.toLowerCase()); @@ -287,6 +286,15 @@ export class UserService { keycloakDetails.keycloakUserId.toString() ); + if (userInfo?.isHolder) { + const getUserRole = await this.userRepository.getUserRole(UserRole.HOLDER); + + if (!getUserRole) { + throw new NotFoundException(ResponseMessages.user.error.userRoleNotFound); + } + await this.userRepository.storeUserRole(userDetails.id, getUserRole?.id); + } + const realmRoles = await this.clientRegistrationService.getAllRealmRoles(token); const holderRole = realmRoles.filter(role => role.name === OrgRoles.HOLDER); @@ -1192,4 +1200,23 @@ export class UserService { throw error; } } + + async getUserByUserIdInKeycloak(email: string): Promise { + try { + + const userData = await this.userRepository.checkUserExist(email.toLowerCase()); + + if (!userData) { + throw new NotFoundException(ResponseMessages.user.error.notFound); + } + + const token = await this.clientRegistrationService.getManagementToken(userData?.clientId, userData?.clientSecret); + const getClientData = await this.clientRegistrationService.getUserInfoByUserId(userData?.keycloakUserId, token); + + return getClientData; + } catch (error) { + this.logger.error(`In getUserByUserIdInKeycloak : ${JSON.stringify(error)}`); + throw error; + } + } } diff --git a/libs/client-registration/src/client-registration.service.ts b/libs/client-registration/src/client-registration.service.ts index b2234f47a..3c03bd7a7 100644 --- a/libs/client-registration/src/client-registration.service.ts +++ b/libs/client-registration/src/client-registration.service.ts @@ -99,7 +99,10 @@ export class ClientRegistrationService { impersonate: true, manage: true }, - realmRoles: ['mb-user'] + realmRoles: ['mb-user'], + attributes: { + ...(user.isHolder ? { userRole: `${CommonConstants.USER_HOLDER_ROLE}` } : {}) + } }; const registerUserResponse = await this.commonService.httpPost( @@ -901,4 +904,25 @@ export class ClientRegistrationService { return redirectUrls; } + + async getUserInfoByUserId( + userId: string, + token: string + ) { + + const realmName = process.env.KEYCLOAK_REALM; + + const userInfo = await this.commonService.httpGet( + await this.keycloakUrlService.GetUserInfoURL(realmName, userId), + this.getAuthHeader(token) + ); + + this.logger.debug( + `userInfo ${JSON.stringify( + userInfo + )}` + ); + + return userInfo; + } } \ No newline at end of file diff --git a/libs/client-registration/src/dtos/create-user.dto.ts b/libs/client-registration/src/dtos/create-user.dto.ts index d63b2a44a..146abc1e9 100644 --- a/libs/client-registration/src/dtos/create-user.dto.ts +++ b/libs/client-registration/src/dtos/create-user.dto.ts @@ -19,5 +19,5 @@ export class CreateUserDto { clientId?: string; clientSecret?: string; supabaseUserId?: string; - + isHolder?: boolean; } diff --git a/libs/common/src/cast.helper.ts b/libs/common/src/cast.helper.ts index a5d3fb551..d9ca24e50 100644 --- a/libs/common/src/cast.helper.ts +++ b/libs/common/src/cast.helper.ts @@ -346,9 +346,8 @@ export const createOobJsonldIssuancePayload = (JsonldCredentialDetails: IJsonldC export class IsHostPortOrDomainConstraint implements ValidatorConstraintInterface { validate(value: string): boolean { // Regular expression for validating URL with host:port or domain - const hostPortRegex = - /^(http:\/\/|https:\/\/)?(?:(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)):(?:\d{1,5})$/; - const domainRegex = /^(http:\/\/|https:\/\/)?(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/; + const hostPortRegex = /^(http:\/\/|https:\/\/)?(?:(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)):(?:\d{1,5})(\/[^\s]*)?$/; + const domainRegex = /^(http:\/\/|https:\/\/)?(?:localhost|(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,})(:\d{1,5})?(\/[^\s]*)?$/; return hostPortRegex.test(value) || domainRegex.test(value); } diff --git a/libs/common/src/common.constant.ts b/libs/common/src/common.constant.ts index 038e3b0d6..a18b5b7ae 100644 --- a/libs/common/src/common.constant.ts +++ b/libs/common/src/common.constant.ts @@ -119,6 +119,8 @@ export enum CommonConstants { URL_SHAGENT_SEND_ANSWER = '/multi-tenancy/question-answer/answer/#/@', URL_SHAGENT_QUESTION_ANSWER_RECORD = '/multi-tenancy/question-answer/#', URL_SHAGENT_DELETE_SUB_WALLET = '/multi-tenancy/#', + URL_SHAGENT_ACCEPT_PROOF_REQUEST = '/multi-tenancy/proofs/#/accept-request/@', + // PROOF SERVICES URL_SEND_PROOF_REQUEST = '/proofs/request-proof', @@ -277,6 +279,9 @@ export enum CommonConstants { PLATFORM_ADMIN_EMAIL='platform.admin@yopmail.com', PLATFORM_ADMIN_ORG='Platform-admin', PLATFORM_ADMIN_ORG_ROLE='platform_admin', + + USER_HOLDER_ROLE='holder', + //onBoarding Type ONBOARDING_TYPE_ADMIN = 0, @@ -325,6 +330,14 @@ CACHE_SHARED_APIKEY_KEY = "dedicatedApiKey", CACHE_APIKEY_KEY = "sharedApiKey", CACHE_TTL_SECONDS = 604800, +CLOUD_WALLET_GET_PROOF_REQUEST = '/multi-tenancy/proofs', +CLOUD_WALLET_CREATE_CONNECTION_INVITATION = '/multi-tenancy/create-invitation', +CLOUD_WALLET_ACCEPT_PROOF_REQUEST = '/accept-request/', +CLOUD_WALLET_DID_LIST = '/multi-tenancy/dids/', +CLOUD_WALLET_CONNECTION_BY_ID = '/multi-tenancy/connections/', +CLOUD_WALLET_CREDENTIAL = '/multi-tenancy/credentials', +CLOUD_WALLET_BASIC_MESSAGE = '/multi-tenancy/basic-messages/', + // Bulk-issuance BATCH_SIZE = 100, MAX_CONCURRENT_OPERATIONS = 50, @@ -352,7 +365,13 @@ VERIFICATION_SERVICE = 'verification', ECOSYSTEM_SERVICE = 'ecosystem', WEBHOOK_SERVICE = 'webhook', NOTIFICATION_SERVICE = 'notification', -GEO_LOCATION_SERVICE = 'geo-location' +GEO_LOCATION_SERVICE = 'geo-location', +CLOUD_WALLET_SERVICE = 'cloud-wallet', + +//CLOUD WALLET +RECEIVE_INVITATION_BY_URL = '/multi-tenancy/receive-invitation-url/', +ACCEPT_OFFER = '/multi-tenancy/credentials/accept-offer/', +SEED_LENGTH = 32 } export const postgresqlErrorCodes = []; diff --git a/libs/common/src/common.service.ts b/libs/common/src/common.service.ts index e6716eec2..00298689f 100644 --- a/libs/common/src/common.service.ts +++ b/libs/common/src/common.service.ts @@ -18,6 +18,9 @@ import { CommonConstants } from './common.constant'; import { HttpService } from '@nestjs/axios/dist'; import { ResponseService } from '@credebl/response'; import * as dotenv from 'dotenv'; +import { RpcException } from '@nestjs/microservices'; +import { ResponseMessages } from './response-messages'; +import { IOptionalParams } from './interfaces/interface'; dotenv.config(); @Injectable() @@ -392,4 +395,89 @@ export class CommonService { throw new BadRequestException('Invalid Credentials'); } } + dataEncryption(data: string) { + // eslint-disable-next-line no-useless-catch + try { + const encryptedToken = CryptoJS.AES.encrypt(JSON.stringify(data), process.env.CRYPTO_PRIVATE_KEY).toString(); + + return encryptedToken; + } catch (error) { + throw error; + } + } + + handleError(error): Promise { + if (error && error?.status && error?.status?.message && error?.status?.message?.error) { + throw new RpcException({ + message: error?.status?.message?.error?.reason + ? error?.status?.message?.error?.reason + : error?.status?.message?.error, + statusCode: error?.status?.code + }); + } else { + throw new RpcException(error.response ? error.response : error); + } + } + +async checkAgentHealth(baseUrl: string, apiKey: string): Promise { + if (!baseUrl || !apiKey) { + throw new BadRequestException(ResponseMessages.cloudWallet.error.agentDetails); + } + const url = `${baseUrl}${CommonConstants.URL_AGENT_GET_ENDPOINT}`; + try { + const agentHealthCheck = await this.httpGet(url, { + headers: { authorization: apiKey } + }); + if (agentHealthCheck.isInitialized) { + return true; + } + return false; + } catch (error) { + throw new Error; + } +} + +async createDynamicUrl(urlOptions: IOptionalParams): Promise { + try { + const { alias, myDid, outOfBandId, state, theirDid, theirLabel, connectionId, threadId } = urlOptions; + // Create the dynamic URL for Search Criteria + const criteriaParams = []; + + if (alias) { + criteriaParams.push(`alias=${alias}`); + } + if (myDid) { + criteriaParams.push(`myDid=${myDid}`); + } + if (outOfBandId) { + criteriaParams.push(`outOfBandId=${outOfBandId}`); + } + if (state) { + criteriaParams.push(`state=${state}`); + } + if (theirDid) { + criteriaParams.push(`theirDid=${theirDid}`); + } + if (theirLabel) { + criteriaParams.push(`theirLabel=${theirLabel}`); + } + if (threadId) { + criteriaParams.push(`threadId=${threadId}`); + } + if (connectionId) { + criteriaParams.push(`connectionId=${connectionId}`); + } + + if (0 < criteriaParams.length) { + const url: string = `?${criteriaParams.join('&')}`; + return url; + } + + return ''; + } catch (error) { + throw new Error(`Failed to create dynamic URL: ${error.message}`); + } +} + + } diff --git a/libs/common/src/interfaces/cloud-wallet.interface.ts b/libs/common/src/interfaces/cloud-wallet.interface.ts new file mode 100644 index 000000000..bd3a496ff --- /dev/null +++ b/libs/common/src/interfaces/cloud-wallet.interface.ts @@ -0,0 +1,332 @@ +import { CloudWalletType } from '@credebl/enum/enum'; +import { $Enums } from '@prisma/client'; + +export class ICreateCloudWallet { + label: string; + connectionImageUrl?: string; + email?: string; + userId?: string; + } + +export interface ICloudWalletDetails { + label: string; + tenantId: string; + email?: string; + type: CloudWalletType; + createdBy: string; + lastChangedBy: string; + userId: string; + agentEndpoint?: string; + agentApiKey?: string; + key?: string; + connectionImageUrl?: string; + } + +export interface IStoredWalletDetails { + email: string, + connectionImageUrl: string, + createDateTime: Date, + id: string, + tenantId: string, + label: string, + lastChangedDateTime: Date +} + +export interface IReceiveInvitation { + alias?: string; + label?: string; + imageUrl?: string; + autoAcceptConnection?: boolean; + autoAcceptInvitation?: boolean; + reuseConnection?: boolean; + acceptInvitationTimeoutMs?: number; + ourDid?: string; + invitationUrl: string; + email?: string; + userId?: string; +} + +export interface IAcceptOffer { + autoAcceptCredential?: string; + comment?: string; + credentialRecordId: string; + credentialFormats?: object; + email?: string; + userId?: string; +} + +export interface ICreateCloudWalletDid { + seed?: string; + keyType: string; + method: string; + network?: string; + domain?: string; + role?: string; + privatekey?: string; + endpoint?: string; + did?: string; + endorserDid?: string; + email?: string; + userId?: string; +} +export interface IGetStoredWalletInfo { + email: string; + userId: string; + id: string; + type: $Enums.CloudWalletType; + agentEndpoint: string; +} + +export interface IConfigureCloudBaseWallet { + email: string; + walletKey: string; + apiKey: string; + agentEndpoint: string; +} + +export interface IConfigureCloudBaseWalletPayload { + cloudBaseWalletConfigure: IConfigureCloudBaseWallet; + userId: string; +} + +export interface IStoreWalletInfo { + email: string; + key: string; + agentApiKey: string; + agentEndpoint: string; + type: CloudWalletType; + userId: string; + createdBy: string; + lastChangedBy: string +} + +export interface IGetStoredWalletInfo { + email: string; + userId: string; + id: string; + type: $Enums.CloudWalletType; + agentEndpoint: string; +} + +export interface IAcceptProofRequest { + proofRecordId: string; + userId: string; + email: string; + filterByPresentationPreview?: boolean; + filterByNonRevocationRequirements?: boolean; + comment?: string; +} + +export interface IAcceptProofRequestPayload { + acceptProofRequest: IAcceptProofRequest; + userId: string; +} + +export interface IProofByProofId { + proofId: string; + userId: string; +} + +export interface IProofPresentation { + threadId: string; + userId: string; +} + +export interface IGetProofPresentationById { + userId: string; + email: string; + proofRecordId: string; +} + +export interface IGetProofPresentation { + userId: string; + email: string; + threadId: string; +} + +export interface ICloudBaseWalletConfigure { + walletKey: string; + apiKey: string; + agentEndpoint: string; + userId: string; + email: string; +} + +export interface Tags { + connectionId: string; + role: string; + state: string; + threadId: string; +} + +export interface IProofRequestRes { + _tags: Tags; + metadata: unknown; + id: string; + createdAt: string; + protocolVersion: string; + state: string; + role: string; + connectionId: string; + threadId: string; + updatedAt: string; +} + +export interface CloudWallet { + id: string; + label: string; + tenantId: string; + email: string; + type: $Enums.CloudWalletType; + createDateTime: Date; + createdBy: string; + lastChangedDateTime: Date; + lastChangedBy: string; + userId: string; + agentEndpoint: string; + agentApiKey: string; + key: string; + connectionImageUrl: string; +} + +export interface IWalletDetailsForDidList { + userId: string; + email: string; +} + +export interface IConnectionDetailsById { + userId: string; + email: string; + connectionId: string; +} + +export interface ITenantDetail { + userId?: string; + email?: string; + threadId?: string; + connectionId?: string; + state?: string; +} + +export interface ICredentialDetails { + userId: string; + email: string; + credentialRecordId: string; +} +export interface Thread { + pthid: string; + thid: string; +} + +export interface Message { + '@type': string; + '@id': string; + '~thread': Thread; + messageType: string; +} + +export interface Data { + base64?: string; + json?: string; + links?: string[]; + jws?: { + header: object; + signature: string; + protected: string; + }; + sha256?: string; +} + +export interface AppendedAttachment { + id: string; + description: string; + filename: string; + mimeType: string; + lastmodTime: string; + byteCount: number; + data: Data; +} + +export interface ICreateConnection { + label: string; + alias: string; + imageUrl: string; + multiUseInvitation: boolean; + autoAcceptConnection: boolean; + goalCode: string; + goal: string; + handshake: boolean; + handshakeProtocols: string[]; + messages: Message[]; + appendedAttachments: AppendedAttachment[]; + invitationDid: string; + recipientKey: string; + userId: string; + email: string; +} + +export interface Invitation { + '@type': string; + '@id': string; + label: string; + accept: string[]; + handshake_protocols: string[]; + services: Service[]; +} + +export interface Service { + id: string; + type: string; + priority: number; + recipientKeys: string[]; + routingKeys: string[]; + serviceEndpoint: string; +} + +export interface OutOfBandRecord { + _tags: Tags; + metadata: Record; + id: string; + createdAt: string; + outOfBandInvitation: Invitation; + role: string; + state: string; + alias: string; + autoAcceptConnection: boolean; + reusable: boolean; + updatedAt: string; +} + +export interface Tags { + recipientKeyFingerprints: string[]; +} + +export interface IConnectionInvitationResponse { + invitationUrl: string; + invitation: Invitation; + outOfBandRecord: OutOfBandRecord; + invitationDid: string; +} + +export interface GetAllCloudWalletConnections { + outOfBandId?: string; + alias?: string; + myDid?: string; + theirDid?: string; + theirLabel?: string; + email?: string; + userId?: string; +} + +export interface IBasicMessage { + userId: string; + email: string; + connectionId: string; +} + +export interface IBasicMessageDetails { + userId?: string; + email?: string; + content: string; + connectionId: string +} diff --git a/libs/common/src/interfaces/interface.ts b/libs/common/src/interfaces/interface.ts index e1b5804dc..b0c459706 100644 --- a/libs/common/src/interfaces/interface.ts +++ b/libs/common/src/interfaces/interface.ts @@ -11,4 +11,15 @@ export interface IAccessTokenData { refresh_expires_in: number; token_type: string; scope: string; -} \ No newline at end of file +} + +export interface IOptionalParams { + alias?: string; + myDid?: string; + outOfBandId?: string; + state?: string; + theirDid?: string; + theirLabel?: string; + threadId?: string; + connectionId?: string; +} \ No newline at end of file diff --git a/libs/common/src/response-messages/index.ts b/libs/common/src/response-messages/index.ts index 5f3b59612..44cc512b4 100644 --- a/libs/common/src/response-messages/index.ts +++ b/libs/common/src/response-messages/index.ts @@ -51,6 +51,7 @@ export const ResponseMessages = { invalidKeycloakId: 'keycloakId is invalid', invalidEmail: 'Invalid Email Id!', adduser: 'Unable to add user details', + userRoleNotFound: 'User role not found', verifyEmail: 'The verification link has already been sent to your email address. please verify', emailNotVerified: 'The verification link has already been sent to your email address. please verify', userNotRegisterd: 'The user has not yet completed the registration process', @@ -513,5 +514,39 @@ export const ResponseMessages = { stateNotFound: 'No states found for provided countryId.Please provide valid countryId', citiesNotFound: 'No cities found for provided stateId and countryId.Please provide valid stateId and countryId' } + }, + cloudWallet: { + success: { + create: 'Cloud wallet created successfully', + receive:'Received invitation successfully', + configureBaseWallet: 'Successfully configure the base wallet.', + acceptProofRequest: 'Proof request has been successfully accepted.', + createConnection: 'Connection created successfully.', + basicMessage: 'Basic message send successfully', + getProofById: 'Proof presentation has been successfully received.', + getProofPresentation: 'Proof presentations has been successfully received.', + didList: 'DID list fetched sucessfully', + connectionById: 'Connection record fetched successfully', + credentials: 'Credentials fetched successfully', + credentialByRecordId: 'Credential fetched successfully', + connectionList: 'Connection list fetched successfully', + basicMessageByConnectionId: 'Basic message fetched successfully' + }, + error: { + baseWalletNotFound: 'Base wallet configuration not found', + createCloudWallet: 'Error while creating cloud wallet on agent', + encryptCloudWalletKey: 'Error while creating encrypting wallet key', + userExist: 'Wallet already exist for the user', + walletNotExist: 'Wallet not exist for the user', + agentDetails: 'Invalid agent details', + agentNotRunning: 'Agent is not up and running', + receiveInvitation: 'Error while receiving invitation by url', + AcceptOffer: 'Error while invitation by url', + notReachable: 'The agent endpoint is not reachable.', + agentAlreadyExist: 'Agent already exist.', + platformAdminRecordNotFound: 'Platform admin reocrd not exist.', + notFoundBaseWallet: 'The base wallet record is missing.', + walletRecordNotFound: 'Wallet record not found.' + } } -}; +}; \ No newline at end of file diff --git a/libs/enum/src/enum.ts b/libs/enum/src/enum.ts index 9922223c1..b014e6114 100644 --- a/libs/enum/src/enum.ts +++ b/libs/enum/src/enum.ts @@ -240,4 +240,14 @@ export enum LedgerLessConstant { export enum ledgerLessDIDType { DID_KEY = 'did:key', DID_WEB = 'did:web' +} + +export enum CloudWalletType { + BASE_WALLET = 'CLOUD_BASE_WALLET', + SUB_WALLET = 'CLOUD_SUB_WALLET' +} + +export enum UserRole { + DEFAULT_USER = 'DEFAULT_USER', + HOLDER = 'HOLDER' } \ No newline at end of file diff --git a/libs/prisma-service/prisma/data/credebl-master-table.json b/libs/prisma-service/prisma/data/credebl-master-table.json index 17cb6bb79..29ffdcb61 100644 --- a/libs/prisma-service/prisma/data/credebl-master-table.json +++ b/libs/prisma-service/prisma/data/credebl-master-table.json @@ -229,5 +229,14 @@ "did:web": "did:web" } } + ], + + "userRoleData": [ + { + "role": "HOLDER" + }, + { + "role": "DEFAULT_USER" + } ] } \ No newline at end of file diff --git a/libs/prisma-service/prisma/migrations/20240717074318_cloud_wallet_tables/migration.sql b/libs/prisma-service/prisma/migrations/20240717074318_cloud_wallet_tables/migration.sql new file mode 100644 index 000000000..97e5ad527 --- /dev/null +++ b/libs/prisma-service/prisma/migrations/20240717074318_cloud_wallet_tables/migration.sql @@ -0,0 +1,50 @@ +-- CreateEnum +CREATE TYPE "UserRole" AS ENUM ('DEFAULT_USER', 'HOLDER'); + +-- CreateEnum +CREATE TYPE "CloudWalletType" AS ENUM ('CLOUD_BASE_WALLET', 'CLOUD_SUB_WALLET'); + +-- CreateTable +CREATE TABLE "user_role" ( + "id" UUID NOT NULL, + "role" "UserRole" NOT NULL, + + CONSTRAINT "user_role_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "user_role_mapping" ( + "id" UUID NOT NULL, + "userId" UUID NOT NULL, + "userRoleId" UUID NOT NULL, + + CONSTRAINT "user_role_mapping_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "cloud_wallet_user_info" ( + "id" UUID NOT NULL, + "label" TEXT NOT NULL, + "tenantId" TEXT NOT NULL, + "email" VARCHAR(500), + "type" "CloudWalletType" NOT NULL, + "createDateTime" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "createdBy" UUID NOT NULL, + "lastChangedDateTime" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "lastChangedBy" UUID NOT NULL, + "userId" UUID NOT NULL, + + CONSTRAINT "cloud_wallet_user_info_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "cloud_wallet_user_info_email_key" ON "cloud_wallet_user_info"("email"); + +-- AddForeignKey +ALTER TABLE "user_role_mapping" ADD CONSTRAINT "user_role_mapping_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "user_role_mapping" ADD CONSTRAINT "user_role_mapping_userRoleId_fkey" FOREIGN KEY ("userRoleId") REFERENCES "user_role"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "cloud_wallet_user_info" ADD CONSTRAINT "cloud_wallet_user_info_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/libs/prisma-service/prisma/migrations/20240717092122_added_agnet_url_and_apikey_in_cloud_wallet_table/migration.sql b/libs/prisma-service/prisma/migrations/20240717092122_added_agnet_url_and_apikey_in_cloud_wallet_table/migration.sql new file mode 100644 index 000000000..8c09c77ae --- /dev/null +++ b/libs/prisma-service/prisma/migrations/20240717092122_added_agnet_url_and_apikey_in_cloud_wallet_table/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE "cloud_wallet_user_info" ADD COLUMN "agentApiKey" TEXT, +ADD COLUMN "agentEndpoint" TEXT; diff --git a/libs/prisma-service/prisma/migrations/20240717102514_added_key_in_cloud_wallet_table/migration.sql b/libs/prisma-service/prisma/migrations/20240717102514_added_key_in_cloud_wallet_table/migration.sql new file mode 100644 index 000000000..63748e5c6 --- /dev/null +++ b/libs/prisma-service/prisma/migrations/20240717102514_added_key_in_cloud_wallet_table/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "cloud_wallet_user_info" ADD COLUMN "key" TEXT; diff --git a/libs/prisma-service/prisma/migrations/20240717104549_added_connection_image_in_cloud_wallet_table/migration.sql b/libs/prisma-service/prisma/migrations/20240717104549_added_connection_image_in_cloud_wallet_table/migration.sql new file mode 100644 index 000000000..f549f4261 --- /dev/null +++ b/libs/prisma-service/prisma/migrations/20240717104549_added_connection_image_in_cloud_wallet_table/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "cloud_wallet_user_info" ADD COLUMN "connectionImageUrl" TEXT; diff --git a/libs/prisma-service/prisma/migrations/20240718102835_cloud_wallet_info_user_option/migration.sql b/libs/prisma-service/prisma/migrations/20240718102835_cloud_wallet_info_user_option/migration.sql new file mode 100644 index 000000000..0094fbd94 --- /dev/null +++ b/libs/prisma-service/prisma/migrations/20240718102835_cloud_wallet_info_user_option/migration.sql @@ -0,0 +1,10 @@ +-- DropForeignKey +ALTER TABLE "cloud_wallet_user_info" DROP CONSTRAINT "cloud_wallet_user_info_userId_fkey"; + +-- AlterTable +ALTER TABLE "cloud_wallet_user_info" ALTER COLUMN "label" DROP NOT NULL, +ALTER COLUMN "tenantId" DROP NOT NULL, +ALTER COLUMN "userId" DROP NOT NULL; + +-- AddForeignKey +ALTER TABLE "cloud_wallet_user_info" ADD CONSTRAINT "cloud_wallet_user_info_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/libs/prisma-service/prisma/schema.prisma b/libs/prisma-service/prisma/schema.prisma index f23222ffc..f11ad5679 100644 --- a/libs/prisma-service/prisma/schema.prisma +++ b/libs/prisma-service/prisma/schema.prisma @@ -9,29 +9,31 @@ datasource db { } model user { - id String @id(map: "PK_cace4a159ff9f2512dd42373760") @default(uuid()) @db.Uuid - createDateTime DateTime @default(now()) @db.Timestamptz(6) - lastChangedDateTime DateTime @default(now()) @db.Timestamptz(6) - firstName String? @db.VarChar(500) - lastName String? @db.VarChar(500) - email String? @unique(map: "UQ_e12875dfb3b1d92d7d7c5377e22") @db.VarChar(500) - username String? @db.VarChar(500) - verificationCode String? @db.VarChar(500) - isEmailVerified Boolean @default(false) - supabaseUserId String? @db.VarChar(500) - keycloakUserId String? @db.VarChar(500) - clientId String? @db.VarChar(500) - clientSecret String? @db.VarChar(500) - profileImg String? - fidoUserId String? @db.VarChar(1000) - isFidoVerified Boolean @default(false) - publicProfile Boolean @default(false) - password String? @db.VarChar - orgInvitations org_invitations[] - user_activities user_activity[] - userDevices user_devices[] - userOrgRoles user_org_roles[] - token token[] + id String @id(map: "PK_cace4a159ff9f2512dd42373760") @default(uuid()) @db.Uuid + createDateTime DateTime @default(now()) @db.Timestamptz(6) + lastChangedDateTime DateTime @default(now()) @db.Timestamptz(6) + firstName String? @db.VarChar(500) + lastName String? @db.VarChar(500) + email String? @unique(map: "UQ_e12875dfb3b1d92d7d7c5377e22") @db.VarChar(500) + username String? @db.VarChar(500) + verificationCode String? @db.VarChar(500) + isEmailVerified Boolean @default(false) + supabaseUserId String? @db.VarChar(500) + keycloakUserId String? @db.VarChar(500) + clientId String? @db.VarChar(500) + clientSecret String? @db.VarChar(500) + profileImg String? + fidoUserId String? @db.VarChar(1000) + isFidoVerified Boolean @default(false) + publicProfile Boolean @default(false) + password String? @db.VarChar + orgInvitations org_invitations[] + user_activities user_activity[] + userDevices user_devices[] + userOrgRoles user_org_roles[] + token token[] + user_role_mapping user_role_mapping[] + cloud_wallet_user_info cloud_wallet_user_info[] } model token { @@ -106,12 +108,12 @@ model organisation { clientId String? @db.VarChar(500) clientSecret String? @db.VarChar(500) registrationNumber String? @db.VarChar(100) - countryId Int? - countries countries? @relation(fields:[countryId],references:[id]) - stateId Int? - states states? @relation(fields:[stateId],references:[id]) - cityId Int? - cities cities? @relation(fields:[cityId],references:[id]) + countryId Int? + countries countries? @relation(fields: [countryId], references: [id]) + stateId Int? + states states? @relation(fields: [stateId], references: [id]) + cityId Int? + cities cities? @relation(fields: [cityId], references: [id]) connections connections[] credentials credentials[] org_agents org_agents[] @@ -126,7 +128,6 @@ model organisation { ecosystemOrgs ecosystem_orgs[] } - model org_invitations { id String @id @default(uuid()) @db.Uuid createDateTime DateTime @default(now()) @db.Timestamptz(6) @@ -549,37 +550,81 @@ enum RecordType { } model countries { - id Int @id @default(autoincrement()) @map("id") - name String @map("name") - states states[] - cities cities[] - organisation organisation[] + id Int @id @default(autoincrement()) @map("id") + name String @map("name") + states states[] + cities cities[] + organisation organisation[] + @@map("countries") } model states { - id Int @id @default(autoincrement()) @map("id") - name String @map("name") - countryId Int @map("country_id") - countryCode String @map("country_code") - countries countries @relation(fields: [countryId], references: [id], onDelete: Cascade) - cities cities[] - organisation organisation[] + id Int @id @default(autoincrement()) @map("id") + name String @map("name") + countryId Int @map("country_id") + countryCode String @map("country_code") + countries countries @relation(fields: [countryId], references: [id], onDelete: Cascade) + cities cities[] + organisation organisation[] @@map("states") } + model cities { - id Int @id @default(autoincrement()) @map("id") - name String @map("name") - stateId Int @map("state_id") - stateCode String @map("state_code") - countryId Int @map("country_id") - countryCode String @map("country_code") - state states @relation(fields: [stateId], references: [id], onDelete: Cascade) - country countries @relation(fields: [countryId], references: [id], onDelete: Cascade) - organisation organisation[] + id Int @id @default(autoincrement()) @map("id") + name String @map("name") + stateId Int @map("state_id") + stateCode String @map("state_code") + countryId Int @map("country_id") + countryCode String @map("country_code") + state states @relation(fields: [stateId], references: [id], onDelete: Cascade) + country countries @relation(fields: [countryId], references: [id], onDelete: Cascade) + organisation organisation[] @@index([stateId], name: "cities_stateId_idx") @@index([countryId], name: "cities_countryId_idx") @@map("cities") -} \ No newline at end of file +} + +model user_role { + id String @id @default(uuid()) @db.Uuid + role UserRole + user_role_mapping user_role_mapping[] +} + +enum UserRole { + DEFAULT_USER + HOLDER +} + +model user_role_mapping { + id String @id @default(uuid()) @db.Uuid + userId String @db.Uuid + user user @relation(fields: [userId], references: [id]) + userRoleId String @db.Uuid + user_role user_role @relation(fields: [userRoleId], references: [id]) +} + +model cloud_wallet_user_info { + id String @id @default(uuid()) @db.Uuid + label String? + tenantId String? + email String? @unique() @db.VarChar(500) + type CloudWalletType + createDateTime DateTime @default(now()) @db.Timestamptz(6) + createdBy String @db.Uuid + lastChangedDateTime DateTime @default(now()) @db.Timestamptz(6) + lastChangedBy String @db.Uuid + userId String? @db.Uuid + agentEndpoint String? + agentApiKey String? + key String? + connectionImageUrl String? + user user? @relation(fields: [userId], references: [id]) +} + +enum CloudWalletType { + CLOUD_BASE_WALLET + CLOUD_SUB_WALLET +} diff --git a/libs/prisma-service/prisma/seed.ts b/libs/prisma-service/prisma/seed.ts index 239cbaab6..67fa762ba 100644 --- a/libs/prisma-service/prisma/seed.ts +++ b/libs/prisma-service/prisma/seed.ts @@ -345,6 +345,34 @@ const createLedgerConfig = async (): Promise => { } }; +const createUserRole = async (): Promise => { + try { + const { userRoleData } = JSON.parse(configData); + + const userRoleDetails = userRoleData.map(userRole => userRole.role); + const existUserRole = await prisma.user_role.findMany({ + where: { + role: { + in: userRoleDetails + } + } + }); + + if (0 === existUserRole.length) { + const userRole = await prisma.user_role.createMany({ + data: userRoleData + }); + + logger.log(userRole); + } else { + logger.log('Already seeding in user role'); + } + + + } catch (e) { + logger.error('An error occurred seeding user role:', e); + } +}; async function main(): Promise { @@ -359,6 +387,7 @@ async function main(): Promise { await createEcosystemRoles(); await createEcosystemConfig(); await createLedgerConfig(); + await createUserRole(); } diff --git a/nest-cli.json b/nest-cli.json index 778bef64b..c14b712ea 100644 --- a/nest-cli.json +++ b/nest-cli.json @@ -295,6 +295,15 @@ "compilerOptions": { "tsConfigPath": "apps/geo-location/tsconfig.app.json" } + }, + "cloud-wallet": { + "type": "application", + "root": "apps/cloud-wallet", + "entryFile": "main", + "sourceRoot": "apps/cloud-wallet/src", + "compilerOptions": { + "tsConfigPath": "apps/cloud-wallet/tsconfig.app.json" + } } } } \ No newline at end of file