diff --git a/apps/api-gateway/src/connection/connection.controller.ts b/apps/api-gateway/src/connection/connection.controller.ts index e091a880a..3bd48a5e4 100644 --- a/apps/api-gateway/src/connection/connection.controller.ts +++ b/apps/api-gateway/src/connection/connection.controller.ts @@ -1,6 +1,6 @@ import { IResponse } from '@credebl/common/interfaces/response.interface'; import { ResponseMessages } from '@credebl/common/response-messages'; -import { Controller, Post, Logger, Body, UseGuards, HttpStatus, Res, Get, Param, UseFilters, Query, Inject, ParseUUIDPipe, BadRequestException } from '@nestjs/common'; +import { Controller, Post, Logger, Body, UseGuards, HttpStatus, Res, Get, Param, UseFilters, Query, Inject, ParseUUIDPipe, BadRequestException, Delete } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; import { ApiBearerAuth, ApiExcludeEndpoint, ApiForbiddenResponse, ApiOperation, ApiQuery, ApiResponse, ApiTags, ApiUnauthorizedResponse } from '@nestjs/swagger'; import { User } from '../authz/decorators/user.decorator'; @@ -21,7 +21,8 @@ import { IConnectionSearchCriteria } from '../interfaces/IConnectionSearch.inter import { SortFields } from 'apps/connection/src/enum/connection.enum'; import { ClientProxy} from '@nestjs/microservices'; import { QuestionAnswerWebhookDto, QuestionDto} from './dtos/question-answer.dto'; - +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import { user } from '@prisma/client'; @UseFilters(CustomExceptionFilter) @Controller() @ApiTags('connections') @@ -332,4 +333,31 @@ export class ConnectionController { } return res.status(HttpStatus.CREATED).json(finalResponse); } + + @Delete('/:orgId/connections') + @ApiOperation({ summary: 'Delete connection record', description: 'Delete connections by orgId' }) + @ApiResponse({ status: HttpStatus.OK, description: 'Success', type: ApiResponseDto }) + @ApiBearerAuth() + @Roles(OrgRoles.OWNER) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + async deleteConnectionsByOrgId( + @Param( + 'orgId', + new ParseUUIDPipe({ + exceptionFactory: (): Error => { + throw new BadRequestException(ResponseMessages.organisation.error.invalidOrgId); + } + }) + ) + orgId: string, + @User() user: user, + @Res() res: Response + ): Promise { + await this.connectionService.deleteConnectionRecords(orgId, user); + const finalResponse: IResponse = { + statusCode: HttpStatus.OK, + message: ResponseMessages.connection.success.deleteConnectionRecord + }; + return res.status(HttpStatus.OK).json(finalResponse); + } } diff --git a/apps/api-gateway/src/connection/connection.service.ts b/apps/api-gateway/src/connection/connection.service.ts index b8566168d..3a43f18ee 100644 --- a/apps/api-gateway/src/connection/connection.service.ts +++ b/apps/api-gateway/src/connection/connection.service.ts @@ -4,10 +4,10 @@ import { ClientProxy, RpcException } from '@nestjs/microservices'; import { BaseService } from 'libs/service/base.service'; import { ConnectionDto, CreateOutOfBandConnectionInvitation, ReceiveInvitationDto, ReceiveInvitationUrlDto } from './dtos/connection.dto'; import { IReceiveInvitationRes, IUserRequestInterface } from './interfaces'; -import { IConnectionList } from '@credebl/common/interfaces/connection.interface'; +import { IConnectionList, IDeletedConnectionsRecord } from '@credebl/common/interfaces/connection.interface'; import { AgentConnectionSearchCriteria, IConnectionDetailsById, IConnectionSearchCriteria } from '../interfaces/IConnectionSearch.interface'; import { QuestionDto } from './dtos/question-answer.dto'; - +import { user } from '@prisma/client'; @Injectable() export class ConnectionService extends BaseService { constructor(@Inject('NATS_CLIENT') private readonly connectionServiceProxy: ClientProxy) { @@ -130,4 +130,9 @@ export class ConnectionService extends BaseService { const payload = { user, createOutOfBandConnectionInvitation }; return this.sendNatsMessage(this.connectionServiceProxy, 'create-connection-invitation', payload); } + + async deleteConnectionRecords(orgId: string, userDetails: user): Promise { + const payload = { orgId, userDetails }; + return this.sendNatsMessage(this.connectionServiceProxy, 'delete-connection-records', payload); + } } diff --git a/apps/api-gateway/src/connection/enums/connections.enum.ts b/apps/api-gateway/src/connection/enums/connections.enum.ts index 1c4a24600..a15929cf6 100644 --- a/apps/api-gateway/src/connection/enums/connections.enum.ts +++ b/apps/api-gateway/src/connection/enums/connections.enum.ts @@ -1,16 +1,3 @@ -export enum Connections { - start = 'start', - invitationSent = 'invitation-sent', - invitationReceived = 'invitation-received', - requestSent = 'request-sent', - declined = 'decliend', - requestReceived = 'request-received', - responseSent = 'response-sent', - responseReceived = 'response-received', - complete = 'complete', - abandoned = 'abandoned' -} - export declare enum HandshakeProtocol { Connections = "https://didcomm.org/connections/1.0", DidExchange = "https://didcomm.org/didexchange/1.0" diff --git a/apps/connection/src/connection.controller.ts b/apps/connection/src/connection.controller.ts index 356458e2a..2be981052 100644 --- a/apps/connection/src/connection.controller.ts +++ b/apps/connection/src/connection.controller.ts @@ -11,10 +11,10 @@ import { IReceiveInvitationByUrlOrg, IReceiveInvitationResponse } from './interfaces/connection.interfaces'; -import { IConnectionList } from '@credebl/common/interfaces/connection.interface'; +import { IConnectionList, IDeletedConnectionsRecord } from '@credebl/common/interfaces/connection.interface'; import { IConnectionDetailsById } from 'apps/api-gateway/src/interfaces/IConnectionSearch.interface'; import { IQuestionPayload } from './interfaces/question-answer.interfaces'; - +import { user } from '@prisma/client'; @Controller() export class ConnectionController { constructor(private readonly connectionService: ConnectionService) {} @@ -95,4 +95,10 @@ export class ConnectionController { async createConnectionInvitation(payload: ICreateOutOfbandConnectionInvitation): Promise { return this.connectionService.createConnectionInvitation(payload); } + + @MessagePattern({ cmd: 'delete-connection-records' }) + async deleteConnectionRecords(payload: {orgId: string, userDetails: user}): Promise { + const { orgId, userDetails } = payload; + return this.connectionService.deleteConnectionRecords(orgId, userDetails); + } } diff --git a/apps/connection/src/connection.module.ts b/apps/connection/src/connection.module.ts index 5182aa12d..4fa3b7abe 100644 --- a/apps/connection/src/connection.module.ts +++ b/apps/connection/src/connection.module.ts @@ -8,6 +8,7 @@ import { ConnectionRepository } from './connection.repository'; import { PrismaService } from '@credebl/prisma-service'; import { CacheModule } from '@nestjs/cache-manager'; import { getNatsOptions } from '@credebl/common/nats.config'; +import { UserActivityRepository } from 'libs/user-activity/repositories'; // import { nkeyAuthenticator } from 'nats'; @Module({ @@ -24,6 +25,6 @@ import { getNatsOptions } from '@credebl/common/nats.config'; CacheModule.register() ], controllers: [ConnectionController], - providers: [ConnectionService, ConnectionRepository, PrismaService, Logger] + providers: [ConnectionService, ConnectionRepository, UserActivityRepository, PrismaService, Logger] }) export class ConnectionModule { } diff --git a/apps/connection/src/connection.repository.ts b/apps/connection/src/connection.repository.ts index 7b784aeb4..834064b0d 100644 --- a/apps/connection/src/connection.repository.ts +++ b/apps/connection/src/connection.repository.ts @@ -1,10 +1,10 @@ -import { Injectable, Logger } from '@nestjs/common'; +import { Injectable, Logger, ConflictException } from '@nestjs/common'; import { PrismaService } from '@credebl/prisma-service'; // eslint-disable-next-line camelcase import { agent_invitations, org_agents, platform_config, shortening_url } from '@prisma/client'; import { IConnectionSearchCriteria, ICreateConnection, OrgAgent } from './interfaces/connection.interfaces'; import { IUserRequest } from '@credebl/user-request/user-request.interface'; -import { IConnectionsListCount } from '@credebl/common/interfaces/connection.interface'; +import { IConnectionsListCount, IDeletedConnectionsRecord } from '@credebl/common/interfaces/connection.interface'; import { SortValue } from '@credebl/enum/enum'; // import { OrgAgent } from './interfaces/connection.interfaces'; @Injectable() @@ -316,4 +316,52 @@ export class ConnectionRepository { throw error; } } + + async deleteConnectionRecordsByOrgId(orgId: string): Promise { + const tablesToCheck = ['credentials', 'presentations']; + + try { + return await this.prisma.$transaction(async (prisma) => { + const referenceCounts = await Promise.all( + tablesToCheck.map((table) => prisma[table].count({ where: { orgId } })) + ); + + const referencedTables = referenceCounts + .map((count, index) => (0 < count ? tablesToCheck[index] : null)) + .filter(Boolean); + + if (0 < referencedTables.length) { + throw new ConflictException(`Organization ID ${orgId} is referenced in the following table(s): ${referencedTables.join(', ')}`); + } + + const getConnectionRecords = await prisma.connections.findMany( + { + where: { + orgId + }, + select: { + createDateTime: true, + createdBy: true, + connectionId: true, + theirLabel: true, + state: true, + orgId: true + + } + }); + + const deleteConnectionRecords = await prisma.connections.deleteMany( + { + where: { + orgId + } + }); + + return {getConnectionRecords, deleteConnectionRecords }; + }); + } catch (error) { + this.logger.error(`Error in deleting connection records: ${error.message}`); + throw error; + } + } } diff --git a/apps/connection/src/connection.service.ts b/apps/connection/src/connection.service.ts index ea4ba2f62..8f5d12e3e 100644 --- a/apps/connection/src/connection.service.ts +++ b/apps/connection/src/connection.service.ts @@ -18,19 +18,21 @@ import { import { ConnectionRepository } from './connection.repository'; import { ResponseMessages } from '@credebl/common/response-messages'; import { IUserRequest } from '@credebl/user-request/user-request.interface'; -import { OrgAgentType } from '@credebl/enum/enum'; +import { OrgAgentType, ConnectionProcessState } from '@credebl/enum/enum'; import { Cache } from 'cache-manager'; import { CACHE_MANAGER } from '@nestjs/cache-manager'; -import { IConnectionList, ICreateConnectionUrl } from '@credebl/common/interfaces/connection.interface'; +import { IConnectionList, ICreateConnectionUrl, IDeletedConnectionsRecord } from '@credebl/common/interfaces/connection.interface'; import { IConnectionDetailsById } from 'apps/api-gateway/src/interfaces/IConnectionSearch.interface'; import { IQuestionPayload } from './interfaces/question-answer.interfaces'; - +import { RecordType, user } from '@prisma/client'; +import { UserActivityRepository } from 'libs/user-activity/repositories'; @Injectable() export class ConnectionService { constructor( private readonly commonService: CommonService, @Inject('NATS_CLIENT') private readonly connectionServiceProxy: ClientProxy, private readonly connectionRepository: ConnectionRepository, + private readonly userActivityRepository: UserActivityRepository, private readonly logger: Logger, @Inject(CACHE_MANAGER) private cacheService: Cache ) {} @@ -779,4 +781,48 @@ export class ConnectionService { throw new RpcException(error.response ? error.response : error); } } + + async deleteConnectionRecords(orgId: string, user: user): Promise { + try { + const deleteConnections = await this.connectionRepository.deleteConnectionRecordsByOrgId(orgId); + + if (0 === deleteConnections?.deleteConnectionRecords?.count) { + throw new NotFoundException(ResponseMessages.connection.error.connectionRecordNotFound); + } + + const statusCounts = { + [ConnectionProcessState.START]: 0, + [ConnectionProcessState.COMPLETE]: 0, + [ConnectionProcessState.ABANDONED]: 0, + [ConnectionProcessState.INVITATION_SENT]: 0, + [ConnectionProcessState.INVITATION_RECEIVED]: 0, + [ConnectionProcessState.REQUEST_SENT]: 0, + [ConnectionProcessState.DECLIEND]: 0, + [ConnectionProcessState.REQUEST_RECEIVED]: 0, + [ConnectionProcessState.RESPONSE_SENT]: 0, + [ConnectionProcessState.RESPONSE_RECEIVED]: 0 + }; + + await Promise.all(deleteConnections.getConnectionRecords.map(async (record) => { + statusCounts[record.state]++; + })); + + const filteredStatusCounts = Object.fromEntries( + Object.entries(statusCounts).filter(entry => 0 < entry[1]) + ); + + const deletedConnectionData = { + deletedConnectionsRecordsCount: deleteConnections?.deleteConnectionRecords?.count, + deletedRecordsStatusCount: filteredStatusCounts + }; + + await this.userActivityRepository._orgDeletedActivity(orgId, user, deletedConnectionData, RecordType.CONNECTION); + + return deleteConnections; + + } catch (error) { + this.logger.error(`[deleteConnectionRecords] - error in deleting connection records: ${JSON.stringify(error)}`); + throw new RpcException(error.response ? error.response : error); + } +} } diff --git a/libs/common/src/interfaces/connection.interface.ts b/libs/common/src/interfaces/connection.interface.ts index 996b48c4c..646b46429 100644 --- a/libs/common/src/interfaces/connection.interface.ts +++ b/libs/common/src/interfaces/connection.interface.ts @@ -33,4 +33,10 @@ export interface IConnectionsListCount { recipientKey?:string; invitationDid?: string } - \ No newline at end of file + +export interface IDeletedConnectionsRecord { + getConnectionRecords: IConnectionItem[]; + deleteConnectionRecords: { + count: number; + }; +} \ 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 2d87e52af..8c0dcae01 100644 --- a/libs/common/src/response-messages/index.ts +++ b/libs/common/src/response-messages/index.ts @@ -263,13 +263,15 @@ export const ResponseMessages = { fetchConnection: 'Connection details fetched successfully', fetch: 'Connections details fetched successfully', questionAnswerRecord: 'Question Answer record fetched successfully', - questionSend:'Question sent successfully' + questionSend:'Question sent successfully', + deleteConnectionRecord: 'Connection records deleted' }, error: { exists: 'Connection is already exist', connectionNotFound: 'Connection not found', agentEndPointNotFound: 'agentEndPoint Not Found', - agentUrlNotFound: 'agent url not found' + agentUrlNotFound: 'agent url not found', + connectionRecordNotFound: 'Connection records not found' } }, issuance: { diff --git a/libs/enum/src/enum.ts b/libs/enum/src/enum.ts index 7ba554f07..7148a0734 100644 --- a/libs/enum/src/enum.ts +++ b/libs/enum/src/enum.ts @@ -156,4 +156,17 @@ export enum VerificationProcessState { DECLIEND = 'declined', ABANDONED = 'abandoned', DONE = 'done' +} + +export enum ConnectionProcessState { + START = 'start', + INVITATION_SENT = 'invitation-sent', + INVITATION_RECEIVED = 'invitation-received', + REQUEST_SENT = 'request-sent', + DECLIEND = 'decliend', + REQUEST_RECEIVED = 'request-received', + RESPONSE_SENT = 'response-sent', + RESPONSE_RECEIVED = 'response-received', + COMPLETE = 'completed', + ABANDONED = 'abandoned' } \ No newline at end of file