From 37dfbc1ce161465ed19ffbae903430aa7ef99131 Mon Sep 17 00:00:00 2001 From: Tipu_Singh Date: Mon, 9 Mar 2026 14:30:42 +0530 Subject: [PATCH 1/7] feat: added API to store intent and notice URL Signed-off-by: Tipu_Singh --- .../dtos/create-intent-notice.dto.ts | 19 ++++++++++ .../oid4vc-verification.controller.ts | 38 +++++++++++++++++++ .../oid4vc-verification.service.ts | 11 ++++++ .../interfaces/intent-notice.interfaces.ts | 5 +++ .../src/oid4vc-verification.controller.ts | 14 +++++++ .../src/oid4vc-verification.repository.ts | 21 ++++++++++ .../src/oid4vc-verification.service.ts | 15 ++++++++ libs/common/src/response-messages/index.ts | 9 +++++ libs/prisma-service/prisma/schema.prisma | 15 ++++++++ 9 files changed, 147 insertions(+) create mode 100644 apps/api-gateway/src/oid4vc-verification/dtos/create-intent-notice.dto.ts create mode 100644 apps/oid4vc-verification/interfaces/intent-notice.interfaces.ts diff --git a/apps/api-gateway/src/oid4vc-verification/dtos/create-intent-notice.dto.ts b/apps/api-gateway/src/oid4vc-verification/dtos/create-intent-notice.dto.ts new file mode 100644 index 000000000..e1dc26b15 --- /dev/null +++ b/apps/api-gateway/src/oid4vc-verification/dtos/create-intent-notice.dto.ts @@ -0,0 +1,19 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsDefined, IsString, IsUrl, IsUUID } from 'class-validator'; + +export class CreateIntentNoticeDto { + @ApiProperty({ description: 'Intent ID to associate the notice with', example: 'uuid-of-intent' }) + @IsDefined() + @IsUUID() + intentId: string; + + @ApiProperty({ description: 'Notice ID', example: 'notice-123' }) + @IsDefined() + @IsString() + noticeId: string; + + @ApiProperty({ description: 'URL of the notice', example: 'https://example.com/notice' }) + @IsDefined() + @IsUrl() + noticeUrl: string; +} diff --git a/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.controller.ts b/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.controller.ts index db9a2b2d0..4863d2406 100644 --- a/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.controller.ts +++ b/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.controller.ts @@ -50,6 +50,7 @@ import { PresentationRequestDto, VerificationPresentationQueryDto } from './dtos import { Oid4vpPresentationWhDto } from '../oid4vc-issuance/dtos/oid4vp-presentation-wh.dto'; import { CreateVerificationTemplateDto, UpdateVerificationTemplateDto } from './dtos/verification-template.dto'; import { CreateIntentBasedVerificationDto } from './dtos/create-intent-based-verification.dto'; +import { CreateIntentNoticeDto } from './dtos/create-intent-notice.dto'; import { IWebhookUrlInfo } from '@credebl/common/interfaces/webhook.interface'; import { VerifyAuthorizationResponseDto } from './dtos/verify-authorization-response.dto'; @@ -729,6 +730,43 @@ export class Oid4vcVerificationController { return res.status(HttpStatus.OK).json(finalResponse); } + @Post('/orgs/:orgId/oid4vp/intent-notice') + @ApiOperation({ + summary: 'Create intent notice', + description: 'Stores a notice associated with an intent for the specified organization.' + }) + @ApiResponse({ status: HttpStatus.CREATED, description: 'Intent notice created successfully.', type: ApiResponseDto }) + @ApiBearerAuth() + @Roles(OrgRoles.OWNER) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + async createIntentNotice( + @Param( + 'orgId', + new ParseUUIDPipe({ + exceptionFactory: (): Error => { + throw new BadRequestException(ResponseMessages.organisation.error.invalidOrgId); + } + }) + ) + orgId: string, + @User() user: user, + @Body() createIntentNoticeDto: CreateIntentNoticeDto, + @Res() res: Response + ): Promise { + this.logger.debug(`[createIntentNotice] Called with orgId=${orgId}, user=${user.id}`); + + const result = await this.oid4vcVerificationService.createIntentNotice(createIntentNoticeDto, orgId, user); + + this.logger.debug(`[createIntentNotice] Intent notice created successfully`); + + const finalResponse: IResponse = { + statusCode: HttpStatus.CREATED, + message: ResponseMessages.intentNotice.success.create, + data: result + }; + return res.status(HttpStatus.CREATED).json(finalResponse); + } + @Post('/orgs/:orgId/oid4vp/verify-authorization-response') @ApiOperation({ summary: 'Verify authorization response', diff --git a/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.service.ts b/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.service.ts index 1fe1c4a95..78b494ec4 100644 --- a/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.service.ts +++ b/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.service.ts @@ -9,6 +9,7 @@ import { IPresentationRequest } from '@credebl/common/interfaces/oid4vp-verifica import { Oid4vpPresentationWhDto } from '../oid4vc-issuance/dtos/oid4vp-presentation-wh.dto'; import { CreateVerificationTemplateDto, UpdateVerificationTemplateDto } from './dtos/verification-template.dto'; import { CreateIntentBasedVerificationDto } from './dtos/create-intent-based-verification.dto'; +import { CreateIntentNoticeDto } from './dtos/create-intent-notice.dto'; import { IWebhookUrlInfo } from '@credebl/common/interfaces/webhook.interface'; import { VerifyAuthorizationResponseDto } from './dtos/verify-authorization-response.dto'; @@ -175,6 +176,16 @@ export class Oid4vcVerificationService { this.logger.debug(`[deleteVerificationTemplate] Called with orgId=${orgId}, templateId=${templateId}`); return this.natsClient.sendNatsMessage(this.oid4vpProxy, 'verification-template-delete', payload); } + async createIntentNotice( + createIntentNoticeDto: CreateIntentNoticeDto, + orgId: string, + userDetails: user + ): Promise { + const payload = { createIntentNoticeDto, orgId, userDetails }; + this.logger.debug(`[createIntentNotice] Called with orgId=${orgId}, user=${userDetails?.id}`); + return this.natsClient.sendNatsMessage(this.oid4vpProxy, 'create-intent-notice', payload); + } + async verifyAuthorizationResponse( verifyAuthorizationResponse: VerifyAuthorizationResponseDto, orgId: string diff --git a/apps/oid4vc-verification/interfaces/intent-notice.interfaces.ts b/apps/oid4vc-verification/interfaces/intent-notice.interfaces.ts new file mode 100644 index 000000000..e88d7497e --- /dev/null +++ b/apps/oid4vc-verification/interfaces/intent-notice.interfaces.ts @@ -0,0 +1,5 @@ +export interface CreateIntentNotice { + intentId: string; + noticeId: string; + noticeUrl: string; +} diff --git a/apps/oid4vc-verification/src/oid4vc-verification.controller.ts b/apps/oid4vc-verification/src/oid4vc-verification.controller.ts index 64eeeb825..15907b4d4 100644 --- a/apps/oid4vc-verification/src/oid4vc-verification.controller.ts +++ b/apps/oid4vc-verification/src/oid4vc-verification.controller.ts @@ -14,6 +14,7 @@ import { VerifyAuthorizationResponse } from '../interfaces/oid4vp-verification-sessions.interfaces'; import { CreateVerificationTemplate, UpdateVerificationTemplate } from '../interfaces/verification-template.interfaces'; +import { CreateIntentNotice } from '../interfaces/intent-notice.interfaces'; @Controller() export class Oid4vpVerificationController { @@ -187,6 +188,19 @@ export class Oid4vpVerificationController { return this.oid4vpVerificationService.deleteVerificationTemplate(orgId, templateId); } + @MessagePattern({ cmd: 'create-intent-notice' }) + async createIntentNotice(payload: { + createIntentNoticeDto: CreateIntentNotice; + orgId: string; + userDetails: user; + }): Promise { + const { createIntentNoticeDto, orgId, userDetails } = payload; + this.logger.debug( + `[createIntentNotice] Received 'create-intent-notice' for orgId=${orgId}, user=${userDetails?.id}` + ); + return this.oid4vpVerificationService.createIntentNotice(createIntentNoticeDto, orgId, userDetails); + } + @MessagePattern({ cmd: 'verify-authorization-response' }) async verifyAuthorizationResponse(payload: { verifyAuthorizationResponse: VerifyAuthorizationResponse; diff --git a/apps/oid4vc-verification/src/oid4vc-verification.repository.ts b/apps/oid4vc-verification/src/oid4vc-verification.repository.ts index 77b6abcdc..824d04225 100644 --- a/apps/oid4vc-verification/src/oid4vc-verification.repository.ts +++ b/apps/oid4vc-verification/src/oid4vc-verification.repository.ts @@ -9,6 +9,7 @@ import { Oid4vpPresentationWh } from '../interfaces/oid4vp-verification-sessions import { x5cKeyType, x5cRecordStatus } from '@credebl/enum/enum'; import { X509CertificateRecord } from '@credebl/common/interfaces/x509.interface'; import { CreateVerificationTemplate, UpdateVerificationTemplate } from '../interfaces/verification-template.interfaces'; +import { CreateIntentNotice } from '../interfaces/intent-notice.interfaces'; @Injectable() export class Oid4vpRepository { @@ -335,6 +336,26 @@ export class Oid4vpRepository { } } + async createIntentNotice(createIntentNoticeDto: CreateIntentNotice, userId: string): Promise { + this.logger.debug(`[createIntentNotice] called for intentId=${createIntentNoticeDto.intentId}, userId=${userId}`); + try { + const created = await this.prisma.intent_notices.create({ + data: { + intentId: createIntentNoticeDto.intentId, + noticeId: createIntentNoticeDto.noticeId, + noticeUrl: createIntentNoticeDto.noticeUrl, + createdBy: userId, + lastChangedBy: userId + } + }); + this.logger.debug(`[createIntentNotice] Created intent notice with id=${created.id}`); + return created; + } catch (error) { + this.logger.error(`[createIntentNotice] Error: ${error?.message ?? error}`); + throw error; + } + } + async deleteVerificationTemplate(orgId: string, templateId: string): Promise { this.logger.debug(`[deleteVerificationTemplate] called with orgId=${orgId}, templateId=${templateId}`); try { diff --git a/apps/oid4vc-verification/src/oid4vc-verification.service.ts b/apps/oid4vc-verification/src/oid4vc-verification.service.ts index 644f3d995..6ceda97b7 100644 --- a/apps/oid4vc-verification/src/oid4vc-verification.service.ts +++ b/apps/oid4vc-verification/src/oid4vc-verification.service.ts @@ -40,6 +40,7 @@ import { import { X509CertificateRecord } from '@credebl/common/interfaces/x509.interface'; import { SignerMethodOption, x5cKeyType } from '@credebl/enum/enum'; import { CreateVerificationTemplate, UpdateVerificationTemplate } from '../interfaces/verification-template.interfaces'; +import { CreateIntentNotice } from '../interfaces/intent-notice.interfaces'; @Injectable() export class Oid4vpVerificationService extends BaseService { constructor( @@ -701,6 +702,20 @@ export class Oid4vpVerificationService extends BaseService { } } + async createIntentNotice( + createIntentNoticeDto: CreateIntentNotice, + orgId: string, + userDetails: user + ): Promise { + this.logger.debug(`[createIntentNotice] called for orgId=${orgId}, user=${userDetails?.id ?? 'unknown'}`); + try { + return await this.oid4vpRepository.createIntentNotice(createIntentNoticeDto, userDetails.id); + } catch (error) { + this.logger.error(`[createIntentNotice] Error: ${error?.message ?? error}`); + throw new RpcException(error); + } + } + async verifyAuthorizationResponse( verifyAuthorizationResponse: VerifyAuthorizationResponse, orgId: string diff --git a/libs/common/src/response-messages/index.ts b/libs/common/src/response-messages/index.ts index 0153cccdd..26257908f 100644 --- a/libs/common/src/response-messages/index.ts +++ b/libs/common/src/response-messages/index.ts @@ -755,6 +755,15 @@ export const ResponseMessages = { invalidId: 'Invalid id.' } }, + intentNotice: { + success: { + create: 'Intent notice created successfully.' + }, + error: { + create: 'Error while creating intent notice.', + intentNotFound: 'Intent not found.' + } + }, x509: { success: { create: 'x509 certificate created successfully', diff --git a/libs/prisma-service/prisma/schema.prisma b/libs/prisma-service/prisma/schema.prisma index c878d63cd..d2252ceda 100755 --- a/libs/prisma-service/prisma/schema.prisma +++ b/libs/prisma-service/prisma/schema.prisma @@ -718,6 +718,7 @@ model intents { lastChangedBy String @db.Uuid ecosystem ecosystem @relation(fields: [ecosystemId], references: [id]) intentTemplates intent_templates[] + intentNotices intent_notices[] } model intent_templates { @@ -738,6 +739,20 @@ model intent_templates { @@index([templateId]) } +model intent_notices { + id String @id @default(uuid()) @db.Uuid + intentId String @db.Uuid + noticeId String + noticeUrl String + createDateTime DateTime @default(now()) @db.Timestamptz(6) + createdBy String @db.Uuid + lastChangedDateTime DateTime @default(now()) @db.Timestamptz(6) + lastChangedBy String + intent intents @relation(fields: [intentId], references: [id]) + + @@index([intentId]) +} + model ecosystem { id String @id @default(uuid()) @db.Uuid name String From b0befd1a8afa0b5f146b1cc8ec92086fa76000af Mon Sep 17 00:00:00 2001 From: Tipu_Singh Date: Thu, 12 Mar 2026 12:53:16 +0530 Subject: [PATCH 2/7] feat: added intent notice related changes Signed-off-by: Tipu_Singh --- .../src/ecosystem/ecosystem.service.ts | 39 ++++ .../src/ecosystem/intent/intent.controller.ts | 185 ++++++++++++++++++ .../dtos/create-intent-notice.dto.ts | 26 ++- .../oid4vc-verification.controller.ts | 38 ---- .../oid4vc-verification.service.ts | 11 -- .../repositories/ecosystem.repository.ts | 128 ++++++++++++ apps/ecosystem/src/ecosystem.controller.ts | 58 ++++++ apps/ecosystem/src/ecosystem.service.ts | 151 ++++++++++++++ .../interfaces/intent-notice.interfaces.ts | 5 +- .../src/oid4vc-verification.controller.ts | 14 -- .../src/oid4vc-verification.repository.ts | 21 -- .../src/oid4vc-verification.service.ts | 25 +-- libs/common/src/response-messages/index.ts | 11 +- libs/prisma-service/prisma/schema.prisma | 1 - 14 files changed, 603 insertions(+), 110 deletions(-) diff --git a/apps/api-gateway/src/ecosystem/ecosystem.service.ts b/apps/api-gateway/src/ecosystem/ecosystem.service.ts index 948111655..dd8ca0e68 100755 --- a/apps/api-gateway/src/ecosystem/ecosystem.service.ts +++ b/apps/api-gateway/src/ecosystem/ecosystem.service.ts @@ -19,6 +19,7 @@ import { CreateIntentTemplateDto, UpdateIntentTemplateDto } from '../utilities/d import { GetAllIntentTemplatesDto } from '../utilities/dtos/get-all-intent-templates.dto'; import { IIntentTemplateList } from '@credebl/common/interfaces/intents-template.interface'; import { IPaginationSortingDto, PaginatedResponse } from 'libs/common/src/interfaces/interface'; +import { CreateIntentNoticeDto, UpdateIntentNoticeDto } from '../oid4vc-verification/dtos/create-intent-notice.dto'; @Injectable() export class EcosystemService { @@ -251,4 +252,42 @@ export class EcosystemService { async getCreateEcosystemInvitationStatus(email: string, status: Invitation): Promise { return this.natsClient.sendNatsMessage(this.serviceProxy, 'get-ecosystem-created-status', { email, status }); } + + async createIntentNotice(createIntentNoticeDto: CreateIntentNoticeDto, userDetails: user): Promise { + const payload = { createIntentNoticeDto, userDetails }; + return this.natsClient.sendNatsMessage(this.serviceProxy, 'create-intent-notice', payload); + } + + async getIntentNoticeById(id: string): Promise { + return this.natsClient.sendNatsMessage(this.serviceProxy, 'get-intent-notice', { id }); + } + + async getIntentNotices(intentId?: string): Promise { + return this.natsClient.sendNatsMessage(this.serviceProxy, 'get-intent-notices', { intentId }); + } + + async getIntentNoticesByEcosystemId( + ecosystemId: string, + pageNumber: number, + pageSize: number, + search: string, + intentId?: string + ): Promise { + return this.natsClient.sendNatsMessage(this.serviceProxy, 'get-intent-notices-by-ecosystem', { + ecosystemId, + pageNumber, + pageSize, + search, + intentId + }); + } + + async updateIntentNotice(updateIntentNoticeDto: UpdateIntentNoticeDto, userDetails: user): Promise { + const payload = { updateIntentNoticeDto, userDetails }; + return this.natsClient.sendNatsMessage(this.serviceProxy, 'update-intent-notice', payload); + } + + async deleteIntentNotice(id: string): Promise { + return this.natsClient.sendNatsMessage(this.serviceProxy, 'delete-intent-notice', { id }); + } } diff --git a/apps/api-gateway/src/ecosystem/intent/intent.controller.ts b/apps/api-gateway/src/ecosystem/intent/intent.controller.ts index a46a63dbe..6666e4b9b 100755 --- a/apps/api-gateway/src/ecosystem/intent/intent.controller.ts +++ b/apps/api-gateway/src/ecosystem/intent/intent.controller.ts @@ -47,6 +47,7 @@ import { PaginationDto } from '@credebl/common/dtos/pagination.dto'; import { TrimStringParamPipe } from '@credebl/common/cast.helper'; import { EcosystemService } from '../ecosystem.service'; import { ForbiddenErrorDto } from '../../dtos/forbidden-error.dto'; +import { CreateIntentNoticeDto, UpdateIntentNoticeDto } from '../../oid4vc-verification/dtos/create-intent-notice.dto'; @UseFilters(CustomExceptionFilter) @Controller('intent') @@ -611,4 +612,188 @@ export class IntentController { }; return res.status(HttpStatus.OK).json(finalResponse); } + + @Post('/notice') + @ApiBearerAuth() + @Roles(OrgRoles.OWNER) + @UseGuards(AuthGuard('jwt'), EcosystemRolesGuard) + @ApiOperation({ summary: 'Create intent notice', description: 'Stores a notice URL associated with an intent.' }) + @ApiResponse({ status: HttpStatus.CREATED, description: 'Intent notice created successfully', type: ApiResponseDto }) + async createIntentNotice( + @Body() createIntentNoticeDto: CreateIntentNoticeDto, + @User() user: PrismaUser, + @Res() res: Response + ): Promise { + const result = await this.ecosystemService.createIntentNotice(createIntentNoticeDto, user); + const finalResponse: IResponse = { + statusCode: HttpStatus.CREATED, + message: ResponseMessages.intentNotice.success.create, + data: result + }; + return res.status(HttpStatus.CREATED).json(finalResponse); + } + + @Get('/notice') + @ApiBearerAuth() + @Roles(OrgRoles.OWNER) + @UseGuards(AuthGuard('jwt'), EcosystemRolesGuard) + @ApiOperation({ + summary: 'Get intent notices', + description: 'Retrieves all intent notices, optionally filtered by intentId.' + }) + @ApiQuery({ name: 'intentId', required: false, type: String, description: 'Filter by intent UUID (optional)' }) + @ApiResponse({ status: HttpStatus.OK, description: 'Intent notices fetched successfully', type: ApiResponseDto }) + async getIntentNotices( + @Res() res: Response, + @Query( + 'intentId', + new ParseUUIDPipe({ + version: '4', + optional: true, + exceptionFactory: (): Error => { + throw new BadRequestException('Invalid intent ID'); + } + }) + ) + intentId?: string + ): Promise { + const result = await this.ecosystemService.getIntentNotices(intentId); + const finalResponse: IResponse = { + statusCode: HttpStatus.OK, + message: ResponseMessages.intentNotice.success.fetchAll, + data: result + }; + return res.status(HttpStatus.OK).json(finalResponse); + } + + @Get('/ecosystem/:ecosystemId/notice') + @ApiBearerAuth() + @Roles(OrgRoles.OWNER) + @UseGuards(AuthGuard('jwt'), EcosystemRolesGuard) + @ApiOperation({ + summary: 'Get intent notices by ecosystem', + description: 'Retrieves all intent notices for an ecosystem with pagination, search, and optional intent filter.' + }) + @ApiQuery({ name: 'pageNumber', required: false, type: Number, description: 'Page number (default: 1)' }) + @ApiQuery({ name: 'pageSize', required: false, type: Number, description: 'Page size (default: 10, max: 100)' }) + @ApiQuery({ name: 'search', required: false, type: String, description: 'Search by notice URL' }) + @ApiQuery({ name: 'intentId', required: false, type: String, description: 'Filter by intent UUID' }) + @ApiResponse({ status: HttpStatus.OK, description: 'Intent notices fetched successfully', type: ApiResponseDto }) + async getIntentNoticesByEcosystemId( + @Param( + 'ecosystemId', + TrimStringParamPipe, + new ParseUUIDPipe({ + exceptionFactory: (): Error => { + throw new BadRequestException(ResponseMessages.ecosystem.error.invalidFormatOfEcosystemId); + } + }) + ) + ecosystemId: string, + @Query() pageDto: PaginationDto, + @Query( + 'intentId', + new ParseUUIDPipe({ + version: '4', + optional: true, + exceptionFactory: (): Error => { + throw new BadRequestException('Invalid intent ID'); + } + }) + ) + intentId: string, + @Res() res: Response + ): Promise { + const result = await this.ecosystemService.getIntentNoticesByEcosystemId( + ecosystemId, + pageDto.pageNumber, + pageDto.pageSize, + pageDto.search, + intentId + ); + return res.status(HttpStatus.OK).json({ + statusCode: HttpStatus.OK, + message: ResponseMessages.intentNotice.success.fetchAll, + data: result + }); + } + + @Get('/notice/:id') + @ApiBearerAuth() + @Roles(OrgRoles.OWNER) + @UseGuards(AuthGuard('jwt'), EcosystemRolesGuard) + @ApiOperation({ summary: 'Get intent notice by ID', description: 'Retrieves a specific intent notice by its ID.' }) + @ApiResponse({ status: HttpStatus.OK, description: 'Intent notice fetched successfully', type: ApiResponseDto }) + async getIntentNoticeById( + @Param( + 'id', + TrimStringParamPipe, + new ParseUUIDPipe({ + exceptionFactory: (): Error => { + throw new BadRequestException('Invalid notice ID'); + } + }) + ) + id: string, + @Res() res: Response + ): Promise { + const result = await this.ecosystemService.getIntentNoticeById(id); + const finalResponse: IResponse = { + statusCode: HttpStatus.OK, + message: ResponseMessages.intentNotice.success.fetch, + data: result + }; + return res.status(HttpStatus.OK).json(finalResponse); + } + + @Put('/notice') + @ApiBearerAuth() + @Roles(OrgRoles.OWNER) + @UseGuards(AuthGuard('jwt'), EcosystemRolesGuard) + @ApiOperation({ + summary: 'Update intent notice', + description: 'Updates the notice URL for an intent. The intentId in the body identifies which notice to update.' + }) + @ApiResponse({ status: HttpStatus.OK, description: 'Intent notice updated successfully', type: ApiResponseDto }) + async updateIntentNotice( + @Body() updateIntentNoticeDto: UpdateIntentNoticeDto, + @User() user: PrismaUser, + @Res() res: Response + ): Promise { + const result = await this.ecosystemService.updateIntentNotice(updateIntentNoticeDto, user); + const finalResponse: IResponse = { + statusCode: HttpStatus.OK, + message: ResponseMessages.intentNotice.success.update, + data: result + }; + return res.status(HttpStatus.OK).json(finalResponse); + } + + @Delete('/notice/:id') + @ApiBearerAuth() + @Roles(OrgRoles.OWNER) + @UseGuards(AuthGuard('jwt'), EcosystemRolesGuard) + @ApiOperation({ summary: 'Delete intent notice', description: 'Deletes an intent notice by its ID.' }) + @ApiResponse({ status: HttpStatus.OK, description: 'Intent notice deleted successfully', type: ApiResponseDto }) + async deleteIntentNotice( + @Param( + 'id', + TrimStringParamPipe, + new ParseUUIDPipe({ + exceptionFactory: (): Error => { + throw new BadRequestException('Invalid notice ID'); + } + }) + ) + id: string, + @Res() res: Response + ): Promise { + const result = await this.ecosystemService.deleteIntentNotice(id); + const finalResponse: IResponse = { + statusCode: HttpStatus.OK, + message: ResponseMessages.intentNotice.success.delete, + data: result + }; + return res.status(HttpStatus.OK).json(finalResponse); + } } diff --git a/apps/api-gateway/src/oid4vc-verification/dtos/create-intent-notice.dto.ts b/apps/api-gateway/src/oid4vc-verification/dtos/create-intent-notice.dto.ts index e1dc26b15..cd4e7448e 100644 --- a/apps/api-gateway/src/oid4vc-verification/dtos/create-intent-notice.dto.ts +++ b/apps/api-gateway/src/oid4vc-verification/dtos/create-intent-notice.dto.ts @@ -1,5 +1,5 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsDefined, IsString, IsUrl, IsUUID } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IsDefined, IsOptional, IsUrl, IsUUID } from 'class-validator'; export class CreateIntentNoticeDto { @ApiProperty({ description: 'Intent ID to associate the notice with', example: 'uuid-of-intent' }) @@ -7,13 +7,25 @@ export class CreateIntentNoticeDto { @IsUUID() intentId: string; - @ApiProperty({ description: 'Notice ID', example: 'notice-123' }) - @IsDefined() - @IsString() - noticeId: string; - @ApiProperty({ description: 'URL of the notice', example: 'https://example.com/notice' }) @IsDefined() @IsUrl() noticeUrl: string; + + @ApiPropertyOptional({ description: 'Organization ID (optional)', example: 'uuid-of-org' }) + @IsOptional() + @IsUUID() + orgId?: string; +} + +export class UpdateIntentNoticeDto { + @ApiProperty({ description: 'Intent ID whose notice should be updated', example: 'uuid-of-intent' }) + @IsDefined() + @IsUUID() + intentId: string; + + @ApiPropertyOptional({ description: 'URL of the notice', example: 'https://example.com/notice' }) + @IsOptional() + @IsUrl() + noticeUrl?: string; } diff --git a/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.controller.ts b/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.controller.ts index 4863d2406..db9a2b2d0 100644 --- a/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.controller.ts +++ b/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.controller.ts @@ -50,7 +50,6 @@ import { PresentationRequestDto, VerificationPresentationQueryDto } from './dtos import { Oid4vpPresentationWhDto } from '../oid4vc-issuance/dtos/oid4vp-presentation-wh.dto'; import { CreateVerificationTemplateDto, UpdateVerificationTemplateDto } from './dtos/verification-template.dto'; import { CreateIntentBasedVerificationDto } from './dtos/create-intent-based-verification.dto'; -import { CreateIntentNoticeDto } from './dtos/create-intent-notice.dto'; import { IWebhookUrlInfo } from '@credebl/common/interfaces/webhook.interface'; import { VerifyAuthorizationResponseDto } from './dtos/verify-authorization-response.dto'; @@ -730,43 +729,6 @@ export class Oid4vcVerificationController { return res.status(HttpStatus.OK).json(finalResponse); } - @Post('/orgs/:orgId/oid4vp/intent-notice') - @ApiOperation({ - summary: 'Create intent notice', - description: 'Stores a notice associated with an intent for the specified organization.' - }) - @ApiResponse({ status: HttpStatus.CREATED, description: 'Intent notice created successfully.', type: ApiResponseDto }) - @ApiBearerAuth() - @Roles(OrgRoles.OWNER) - @UseGuards(AuthGuard('jwt'), OrgRolesGuard) - async createIntentNotice( - @Param( - 'orgId', - new ParseUUIDPipe({ - exceptionFactory: (): Error => { - throw new BadRequestException(ResponseMessages.organisation.error.invalidOrgId); - } - }) - ) - orgId: string, - @User() user: user, - @Body() createIntentNoticeDto: CreateIntentNoticeDto, - @Res() res: Response - ): Promise { - this.logger.debug(`[createIntentNotice] Called with orgId=${orgId}, user=${user.id}`); - - const result = await this.oid4vcVerificationService.createIntentNotice(createIntentNoticeDto, orgId, user); - - this.logger.debug(`[createIntentNotice] Intent notice created successfully`); - - const finalResponse: IResponse = { - statusCode: HttpStatus.CREATED, - message: ResponseMessages.intentNotice.success.create, - data: result - }; - return res.status(HttpStatus.CREATED).json(finalResponse); - } - @Post('/orgs/:orgId/oid4vp/verify-authorization-response') @ApiOperation({ summary: 'Verify authorization response', diff --git a/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.service.ts b/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.service.ts index 78b494ec4..1fe1c4a95 100644 --- a/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.service.ts +++ b/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.service.ts @@ -9,7 +9,6 @@ import { IPresentationRequest } from '@credebl/common/interfaces/oid4vp-verifica import { Oid4vpPresentationWhDto } from '../oid4vc-issuance/dtos/oid4vp-presentation-wh.dto'; import { CreateVerificationTemplateDto, UpdateVerificationTemplateDto } from './dtos/verification-template.dto'; import { CreateIntentBasedVerificationDto } from './dtos/create-intent-based-verification.dto'; -import { CreateIntentNoticeDto } from './dtos/create-intent-notice.dto'; import { IWebhookUrlInfo } from '@credebl/common/interfaces/webhook.interface'; import { VerifyAuthorizationResponseDto } from './dtos/verify-authorization-response.dto'; @@ -176,16 +175,6 @@ export class Oid4vcVerificationService { this.logger.debug(`[deleteVerificationTemplate] Called with orgId=${orgId}, templateId=${templateId}`); return this.natsClient.sendNatsMessage(this.oid4vpProxy, 'verification-template-delete', payload); } - async createIntentNotice( - createIntentNoticeDto: CreateIntentNoticeDto, - orgId: string, - userDetails: user - ): Promise { - const payload = { createIntentNoticeDto, orgId, userDetails }; - this.logger.debug(`[createIntentNotice] Called with orgId=${orgId}, user=${userDetails?.id}`); - return this.natsClient.sendNatsMessage(this.oid4vpProxy, 'create-intent-notice', payload); - } - async verifyAuthorizationResponse( verifyAuthorizationResponse: VerifyAuthorizationResponseDto, orgId: string diff --git a/apps/ecosystem/repositories/ecosystem.repository.ts b/apps/ecosystem/repositories/ecosystem.repository.ts index e8142cb69..fb382e890 100755 --- a/apps/ecosystem/repositories/ecosystem.repository.ts +++ b/apps/ecosystem/repositories/ecosystem.repository.ts @@ -1692,4 +1692,132 @@ export class EcosystemRepository { throw error; } } + + // Intent Notice CRUD + async createIntentNotice(intentId: string, noticeUrl: string, userId: string): Promise { + try { + return await this.prisma.intent_notices.create({ + data: { + noticeId: intentId, + intent: { connect: { id: intentId } }, + noticeUrl, + createdBy: userId, + lastChangedBy: userId + } + }); + } catch (error) { + this.logger.error(`createIntentNotice error: ${error}`); + throw error; + } + } + + async getIntentNoticeById(id: string): Promise { + try { + return await this.prisma.intent_notices.findFirst({ + where: { id } + }); + } catch (error) { + this.logger.error(`getIntentNoticeById error: ${error}`); + throw error; + } + } + + async getIntentNotices(intentId?: string): Promise { + try { + return await this.prisma.intent_notices.findMany({ + where: intentId ? { intentId } : {}, + orderBy: { createDateTime: 'desc' } + }); + } catch (error) { + this.logger.error(`getIntentNotices error: ${error}`); + throw error; + } + } + + async getIntentNoticesByEcosystemId( + ecosystemId: string, + pageNumber: number, + pageSize: number, + search: string, + intentId?: string + ): Promise<{ data: object[]; totalPages: number; totalCount: number }> { + try { + const where = { + intent: { ecosystemId }, + ...(intentId && { intentId }), + ...(search && { noticeUrl: { contains: search, mode: 'insensitive' as const } }) + }; + + const [data, totalCount] = await this.prisma.$transaction([ + this.prisma.intent_notices.findMany({ + where, + include: { intent: { select: { id: true, name: true, ecosystemId: true } } }, + orderBy: { createDateTime: 'desc' }, + skip: (pageNumber - 1) * pageSize, + take: pageSize + }), + this.prisma.intent_notices.count({ where }) + ]); + + return { data, totalPages: Math.ceil(totalCount / pageSize), totalCount }; + } catch (error) { + this.logger.error(`getIntentNoticesByEcosystemId error: ${error}`); + throw error; + } + } + + async getIntentNoticeByIntentId(intentId: string): Promise { + try { + return await this.prisma.intent_notices.findFirst({ where: { intentId } }); + } catch (error) { + this.logger.error(`getIntentNoticeByIntentId error: ${error}`); + throw error; + } + } + + async isEcosystemLead(userId: string, ecosystemId: string): Promise { + try { + const record = await this.prisma.ecosystem_orgs.findFirst({ + where: { + userId, + ecosystemId, + deletedAt: null, + ecosystemRole: { name: EcosystemRoles.ECOSYSTEM_LEAD } + } + }); + return Boolean(record); + } catch (error) { + this.logger.error(`isEcosystemLead error: ${error}`); + throw error; + } + } + + async updateIntentNotice(intentId: string, noticeUrl: string, userId: string): Promise { + try { + const record = await this.prisma.intent_notices.findFirst({ where: { intentId } }); + if (!record) { + return null; + } + return await this.prisma.intent_notices.update({ + where: { id: record['id'] }, + data: { noticeUrl, lastChangedBy: userId } + }); + } catch (error) { + this.logger.error(`updateIntentNotice error: ${error}`); + throw error; + } + } + + async deleteIntentNotice(id: string): Promise { + try { + const record = await this.prisma.intent_notices.findFirst({ where: { id } }); + if (!record) { + return null; + } + return await this.prisma.intent_notices.delete({ where: { id: record['id'] } }); + } catch (error) { + this.logger.error(`deleteIntentNotice error: ${error}`); + throw error; + } + } } diff --git a/apps/ecosystem/src/ecosystem.controller.ts b/apps/ecosystem/src/ecosystem.controller.ts index bb803feea..0e48eace8 100755 --- a/apps/ecosystem/src/ecosystem.controller.ts +++ b/apps/ecosystem/src/ecosystem.controller.ts @@ -354,4 +354,62 @@ export class EcosystemController { async getCreateEcosystemInvitationStatus(payload: { email: string; status: Invitation }): Promise { return this.ecosystemService.getCreateEcosystemInvitationStatus(payload.email, payload.status); } + + // Intent Notice CRUD + @MessagePattern({ cmd: 'create-intent-notice' }) + async createIntentNotice(payload: { + createIntentNoticeDto: { intentId: string; noticeUrl: string }; + userDetails: user; + }): Promise { + const { createIntentNoticeDto, userDetails } = payload; + return this.ecosystemService.createIntentNotice( + createIntentNoticeDto.intentId, + createIntentNoticeDto.noticeUrl, + userDetails.id + ); + } + + @MessagePattern({ cmd: 'get-intent-notice' }) + async getIntentNoticeById(payload: { id: string }): Promise { + return this.ecosystemService.getIntentNoticeById(payload.id); + } + + @MessagePattern({ cmd: 'get-intent-notices' }) + async getIntentNotices(payload: { intentId?: string }): Promise { + return this.ecosystemService.getIntentNotices(payload.intentId); + } + + @MessagePattern({ cmd: 'get-intent-notices-by-ecosystem' }) + async getIntentNoticesByEcosystemId(payload: { + ecosystemId: string; + pageNumber: number; + pageSize: number; + search: string; + intentId?: string; + }): Promise { + const { ecosystemId, pageNumber, pageSize, search, intentId } = payload; + return this.ecosystemService.getIntentNoticesByEcosystemId(ecosystemId, pageNumber, pageSize, search, intentId); + } + + @MessagePattern({ cmd: 'get-intent-notice-by-intent-id' }) + async getIntentNoticeByIntentId(payload: { intentId: string }): Promise { + return this.ecosystemService.getIntentNoticeByIntentId(payload.intentId); + } + + @MessagePattern({ cmd: 'update-intent-notice' }) + async updateIntentNotice(payload: { + updateIntentNoticeDto: { intentId: string; noticeUrl?: string }; + userDetails: user; + }): Promise { + return this.ecosystemService.updateIntentNotice( + payload.updateIntentNoticeDto.intentId, + payload.updateIntentNoticeDto.noticeUrl, + payload.userDetails.id + ); + } + + @MessagePattern({ cmd: 'delete-intent-notice' }) + async deleteIntentNotice(payload: { id: string }): Promise { + return this.ecosystemService.deleteIntentNotice(payload.id); + } } diff --git a/apps/ecosystem/src/ecosystem.service.ts b/apps/ecosystem/src/ecosystem.service.ts index 6825f44fa..c0c53a061 100755 --- a/apps/ecosystem/src/ecosystem.service.ts +++ b/apps/ecosystem/src/ecosystem.service.ts @@ -1005,4 +1005,155 @@ export class EcosystemService { async getCreateEcosystemInvitationStatus(email: string, status: Invitation): Promise { return this.ecosystemRepository.getCreateEcosystemInvitationStatus(email, status); } + + // Intent Notice CRUD + + private async validateEcosystemLead(userId: string, ecosystemId: string): Promise { + const isLead = await this.ecosystemRepository.isEcosystemLead(userId, ecosystemId); + if (!isLead) { + throw new RpcException({ + statusCode: HttpStatus.FORBIDDEN, + message: 'Only Ecosystem Lead can perform this action.' + }); + } + } + + async createIntentNotice(intentId: string, noticeUrl: string, userId: string): Promise { + try { + const intent = await this.ecosystemRepository.findIntentById(intentId); + if (!intent) { + throw new RpcException({ + statusCode: HttpStatus.NOT_FOUND, + message: ResponseMessages.intentNotice.error.intentNotFound + }); + } + await this.validateEcosystemLead(userId, intent['ecosystemId']); + return await this.ecosystemRepository.createIntentNotice(intentId, noticeUrl, userId); + } catch (error) { + const errorResponse = ErrorHandler.categorize(error, ResponseMessages.intentNotice.error.create); + this.logger.error( + `[createIntentNotice] ${errorResponse.statusCode}: ${errorResponse.message}`, + ErrorHandler.format(error) + ); + throw new RpcException(errorResponse); + } + } + + async getIntentNoticeById(id: string): Promise { + try { + const record = await this.ecosystemRepository.getIntentNoticeById(id); + if (!record) { + throw new RpcException({ + statusCode: HttpStatus.NOT_FOUND, + message: ResponseMessages.intentNotice.error.notFound + }); + } + return record; + } catch (error) { + const errorResponse = ErrorHandler.categorize(error, ResponseMessages.intentNotice.error.notFound); + this.logger.error( + `[getIntentNoticeById] ${errorResponse.statusCode}: ${errorResponse.message}`, + ErrorHandler.format(error) + ); + throw new RpcException(errorResponse); + } + } + + async getIntentNotices(intentId?: string): Promise { + try { + const records = await this.ecosystemRepository.getIntentNotices(intentId); + if (!records || 0 === records.length) { + throw new RpcException({ + statusCode: HttpStatus.NOT_FOUND, + message: ResponseMessages.intentNotice.error.notFound + }); + } + return records; + } catch (error) { + const errorResponse = ErrorHandler.categorize(error, ResponseMessages.intentNotice.error.notFound); + this.logger.error( + `[getIntentNotices] ${errorResponse.statusCode}: ${errorResponse.message}`, + ErrorHandler.format(error) + ); + throw new RpcException(errorResponse); + } + } + + async getIntentNoticesByEcosystemId( + ecosystemId: string, + pageNumber: number, + pageSize: number, + search: string, + intentId?: string + ): Promise { + try { + return await this.ecosystemRepository.getIntentNoticesByEcosystemId( + ecosystemId, + pageNumber, + pageSize, + search, + intentId + ); + } catch (error) { + const errorResponse = ErrorHandler.categorize(error, ResponseMessages.intentNotice.error.notFound); + this.logger.error( + `[getIntentNoticesByEcosystemId] ${errorResponse.statusCode}: ${errorResponse.message}`, + ErrorHandler.format(error) + ); + throw new RpcException(errorResponse); + } + } + + async getIntentNoticeByIntentId(intentId: string): Promise { + try { + return await this.ecosystemRepository.getIntentNoticeByIntentId(intentId); + } catch (error) { + const errorResponse = ErrorHandler.categorize(error, ResponseMessages.intentNotice.error.notFound); + this.logger.error( + `[getIntentNoticeByIntentId] ${errorResponse.statusCode}: ${errorResponse.message}`, + ErrorHandler.format(error) + ); + throw new RpcException(errorResponse); + } + } + + async updateIntentNotice(intentId: string, noticeUrl: string, userId: string): Promise { + try { + const updated = await this.ecosystemRepository.updateIntentNotice(intentId, noticeUrl, userId); + if (!updated) { + throw new RpcException({ + statusCode: HttpStatus.NOT_FOUND, + message: ResponseMessages.intentNotice.error.notFound + }); + } + return updated; + } catch (error) { + const errorResponse = ErrorHandler.categorize(error, ResponseMessages.intentNotice.error.updateFailed); + this.logger.error( + `[updateIntentNotice] ${errorResponse.statusCode}: ${errorResponse.message}`, + ErrorHandler.format(error) + ); + throw new RpcException(errorResponse); + } + } + + async deleteIntentNotice(id: string): Promise { + try { + const deleted = await this.ecosystemRepository.deleteIntentNotice(id); + if (!deleted) { + throw new RpcException({ + statusCode: HttpStatus.NOT_FOUND, + message: ResponseMessages.intentNotice.error.notFound + }); + } + return deleted; + } catch (error) { + const errorResponse = ErrorHandler.categorize(error, ResponseMessages.intentNotice.error.deleteFailed); + this.logger.error( + `[deleteIntentNotice] ${errorResponse.statusCode}: ${errorResponse.message}`, + ErrorHandler.format(error) + ); + throw new RpcException(errorResponse); + } + } } diff --git a/apps/oid4vc-verification/interfaces/intent-notice.interfaces.ts b/apps/oid4vc-verification/interfaces/intent-notice.interfaces.ts index e88d7497e..cf209ecb5 100644 --- a/apps/oid4vc-verification/interfaces/intent-notice.interfaces.ts +++ b/apps/oid4vc-verification/interfaces/intent-notice.interfaces.ts @@ -1,5 +1,8 @@ export interface CreateIntentNotice { intentId: string; - noticeId: string; noticeUrl: string; } + +export interface UpdateIntentNotice { + noticeUrl?: string; +} diff --git a/apps/oid4vc-verification/src/oid4vc-verification.controller.ts b/apps/oid4vc-verification/src/oid4vc-verification.controller.ts index 15907b4d4..64eeeb825 100644 --- a/apps/oid4vc-verification/src/oid4vc-verification.controller.ts +++ b/apps/oid4vc-verification/src/oid4vc-verification.controller.ts @@ -14,7 +14,6 @@ import { VerifyAuthorizationResponse } from '../interfaces/oid4vp-verification-sessions.interfaces'; import { CreateVerificationTemplate, UpdateVerificationTemplate } from '../interfaces/verification-template.interfaces'; -import { CreateIntentNotice } from '../interfaces/intent-notice.interfaces'; @Controller() export class Oid4vpVerificationController { @@ -188,19 +187,6 @@ export class Oid4vpVerificationController { return this.oid4vpVerificationService.deleteVerificationTemplate(orgId, templateId); } - @MessagePattern({ cmd: 'create-intent-notice' }) - async createIntentNotice(payload: { - createIntentNoticeDto: CreateIntentNotice; - orgId: string; - userDetails: user; - }): Promise { - const { createIntentNoticeDto, orgId, userDetails } = payload; - this.logger.debug( - `[createIntentNotice] Received 'create-intent-notice' for orgId=${orgId}, user=${userDetails?.id}` - ); - return this.oid4vpVerificationService.createIntentNotice(createIntentNoticeDto, orgId, userDetails); - } - @MessagePattern({ cmd: 'verify-authorization-response' }) async verifyAuthorizationResponse(payload: { verifyAuthorizationResponse: VerifyAuthorizationResponse; diff --git a/apps/oid4vc-verification/src/oid4vc-verification.repository.ts b/apps/oid4vc-verification/src/oid4vc-verification.repository.ts index 824d04225..77b6abcdc 100644 --- a/apps/oid4vc-verification/src/oid4vc-verification.repository.ts +++ b/apps/oid4vc-verification/src/oid4vc-verification.repository.ts @@ -9,7 +9,6 @@ import { Oid4vpPresentationWh } from '../interfaces/oid4vp-verification-sessions import { x5cKeyType, x5cRecordStatus } from '@credebl/enum/enum'; import { X509CertificateRecord } from '@credebl/common/interfaces/x509.interface'; import { CreateVerificationTemplate, UpdateVerificationTemplate } from '../interfaces/verification-template.interfaces'; -import { CreateIntentNotice } from '../interfaces/intent-notice.interfaces'; @Injectable() export class Oid4vpRepository { @@ -336,26 +335,6 @@ export class Oid4vpRepository { } } - async createIntentNotice(createIntentNoticeDto: CreateIntentNotice, userId: string): Promise { - this.logger.debug(`[createIntentNotice] called for intentId=${createIntentNoticeDto.intentId}, userId=${userId}`); - try { - const created = await this.prisma.intent_notices.create({ - data: { - intentId: createIntentNoticeDto.intentId, - noticeId: createIntentNoticeDto.noticeId, - noticeUrl: createIntentNoticeDto.noticeUrl, - createdBy: userId, - lastChangedBy: userId - } - }); - this.logger.debug(`[createIntentNotice] Created intent notice with id=${created.id}`); - return created; - } catch (error) { - this.logger.error(`[createIntentNotice] Error: ${error?.message ?? error}`); - throw error; - } - } - async deleteVerificationTemplate(orgId: string, templateId: string): Promise { this.logger.debug(`[deleteVerificationTemplate] called with orgId=${orgId}, templateId=${templateId}`); try { diff --git a/apps/oid4vc-verification/src/oid4vc-verification.service.ts b/apps/oid4vc-verification/src/oid4vc-verification.service.ts index 6ceda97b7..511c96d8c 100644 --- a/apps/oid4vc-verification/src/oid4vc-verification.service.ts +++ b/apps/oid4vc-verification/src/oid4vc-verification.service.ts @@ -40,7 +40,6 @@ import { import { X509CertificateRecord } from '@credebl/common/interfaces/x509.interface'; import { SignerMethodOption, x5cKeyType } from '@credebl/enum/enum'; import { CreateVerificationTemplate, UpdateVerificationTemplate } from '../interfaces/verification-template.interfaces'; -import { CreateIntentNotice } from '../interfaces/intent-notice.interfaces'; @Injectable() export class Oid4vpVerificationService extends BaseService { constructor( @@ -406,6 +405,16 @@ export class Oid4vpVerificationService extends BaseService { this.logger.debug( `[createIntentBasedVerificationPresentation] verification presentation created successfully for orgId=${orgId}` ); + if (createdSession) { + const intentNotice: any = await this.natsClient + .sendNatsMessage(this.oid4vpVerificationServiceProxy, 'get-intent-notice-by-intent-id', { intentId: intent }) + .catch(() => null); + + if (intentNotice?.noticeUrl) { + createdSession.noticeUrl = intentNotice.noticeUrl; + } + } + return createdSession; } catch (error) { this.logger.error( @@ -702,20 +711,6 @@ export class Oid4vpVerificationService extends BaseService { } } - async createIntentNotice( - createIntentNoticeDto: CreateIntentNotice, - orgId: string, - userDetails: user - ): Promise { - this.logger.debug(`[createIntentNotice] called for orgId=${orgId}, user=${userDetails?.id ?? 'unknown'}`); - try { - return await this.oid4vpRepository.createIntentNotice(createIntentNoticeDto, userDetails.id); - } catch (error) { - this.logger.error(`[createIntentNotice] Error: ${error?.message ?? error}`); - throw new RpcException(error); - } - } - async verifyAuthorizationResponse( verifyAuthorizationResponse: VerifyAuthorizationResponse, orgId: string diff --git a/libs/common/src/response-messages/index.ts b/libs/common/src/response-messages/index.ts index 26257908f..efee0dc53 100644 --- a/libs/common/src/response-messages/index.ts +++ b/libs/common/src/response-messages/index.ts @@ -757,11 +757,18 @@ export const ResponseMessages = { }, intentNotice: { success: { - create: 'Intent notice created successfully.' + create: 'Intent notice created successfully.', + fetch: 'Intent notice fetched successfully.', + fetchAll: 'Intent notices fetched successfully.', + update: 'Intent notice updated successfully.', + delete: 'Intent notice deleted successfully.' }, error: { create: 'Error while creating intent notice.', - intentNotFound: 'Intent not found.' + intentNotFound: 'Intent not found.', + notFound: 'Intent notice not found.', + updateFailed: 'Error while updating intent notice.', + deleteFailed: 'Error while deleting intent notice.' } }, x509: { diff --git a/libs/prisma-service/prisma/schema.prisma b/libs/prisma-service/prisma/schema.prisma index d2252ceda..d8f43ccb2 100755 --- a/libs/prisma-service/prisma/schema.prisma +++ b/libs/prisma-service/prisma/schema.prisma @@ -742,7 +742,6 @@ model intent_templates { model intent_notices { id String @id @default(uuid()) @db.Uuid intentId String @db.Uuid - noticeId String noticeUrl String createDateTime DateTime @default(now()) @db.Timestamptz(6) createdBy String @db.Uuid From 382a06f05ed22eb2561d8cbd341d877f23b47075 Mon Sep 17 00:00:00 2001 From: Tipu_Singh Date: Mon, 16 Mar 2026 12:10:59 +0530 Subject: [PATCH 3/7] feat: updated intent notice APIs Signed-off-by: Tipu_Singh --- .../src/ecosystem/ecosystem.service.ts | 8 +++- .../src/ecosystem/intent/intent.controller.ts | 28 ++++++++----- .../dtos/create-intent-notice.dto.ts | 9 +---- .../oid4vc-verification.controller.ts | 15 ++++++- .../oid4vc-verification.service.ts | 14 ++++++- .../repositories/ecosystem.repository.ts | 40 ++++++++++++++++--- apps/ecosystem/src/ecosystem.controller.ts | 14 ++++--- apps/ecosystem/src/ecosystem.helper.ts | 4 ++ apps/ecosystem/src/ecosystem.service.ts | 27 +++++++++---- .../src/oid4vc-verification.controller.ts | 7 +++- .../src/oid4vc-verification.service.ts | 23 +++++++---- .../migration.sql | 18 +++++++++ .../migration.sql | 8 ++++ libs/prisma-service/prisma/schema.prisma | 16 +++++--- 14 files changed, 174 insertions(+), 57 deletions(-) create mode 100644 apps/ecosystem/src/ecosystem.helper.ts create mode 100644 libs/prisma-service/prisma/migrations/20260312082536_added_intent_notice_table/migration.sql create mode 100644 libs/prisma-service/prisma/migrations/20260312084927_add_org_id_to_intent_notices/migration.sql diff --git a/apps/api-gateway/src/ecosystem/ecosystem.service.ts b/apps/api-gateway/src/ecosystem/ecosystem.service.ts index dd8ca0e68..46d4ca7ad 100755 --- a/apps/api-gateway/src/ecosystem/ecosystem.service.ts +++ b/apps/api-gateway/src/ecosystem/ecosystem.service.ts @@ -282,8 +282,12 @@ export class EcosystemService { }); } - async updateIntentNotice(updateIntentNoticeDto: UpdateIntentNoticeDto, userDetails: user): Promise { - const payload = { updateIntentNoticeDto, userDetails }; + async updateIntentNotice( + id: string, + updateIntentNoticeDto: UpdateIntentNoticeDto, + userDetails: user + ): Promise { + const payload = { id, updateIntentNoticeDto, userDetails }; return this.natsClient.sendNatsMessage(this.serviceProxy, 'update-intent-notice', payload); } diff --git a/apps/api-gateway/src/ecosystem/intent/intent.controller.ts b/apps/api-gateway/src/ecosystem/intent/intent.controller.ts index 6666e4b9b..fa621c7cf 100755 --- a/apps/api-gateway/src/ecosystem/intent/intent.controller.ts +++ b/apps/api-gateway/src/ecosystem/intent/intent.controller.ts @@ -615,7 +615,7 @@ export class IntentController { @Post('/notice') @ApiBearerAuth() - @Roles(OrgRoles.OWNER) + @Roles(OrgRoles.ECOSYSTEM_LEAD) @UseGuards(AuthGuard('jwt'), EcosystemRolesGuard) @ApiOperation({ summary: 'Create intent notice', description: 'Stores a notice URL associated with an intent.' }) @ApiResponse({ status: HttpStatus.CREATED, description: 'Intent notice created successfully', type: ApiResponseDto }) @@ -635,7 +635,7 @@ export class IntentController { @Get('/notice') @ApiBearerAuth() - @Roles(OrgRoles.OWNER) + @Roles(OrgRoles.ECOSYSTEM_LEAD, OrgRoles.ECOSYSTEM_MEMBER) @UseGuards(AuthGuard('jwt'), EcosystemRolesGuard) @ApiOperation({ summary: 'Get intent notices', @@ -668,7 +668,7 @@ export class IntentController { @Get('/ecosystem/:ecosystemId/notice') @ApiBearerAuth() - @Roles(OrgRoles.OWNER) + @Roles(OrgRoles.ECOSYSTEM_LEAD, OrgRoles.ECOSYSTEM_MEMBER) @UseGuards(AuthGuard('jwt'), EcosystemRolesGuard) @ApiOperation({ summary: 'Get intent notices by ecosystem', @@ -720,7 +720,7 @@ export class IntentController { @Get('/notice/:id') @ApiBearerAuth() - @Roles(OrgRoles.OWNER) + @Roles(OrgRoles.ECOSYSTEM_LEAD, OrgRoles.ECOSYSTEM_MEMBER) @UseGuards(AuthGuard('jwt'), EcosystemRolesGuard) @ApiOperation({ summary: 'Get intent notice by ID', description: 'Retrieves a specific intent notice by its ID.' }) @ApiResponse({ status: HttpStatus.OK, description: 'Intent notice fetched successfully', type: ApiResponseDto }) @@ -746,21 +746,31 @@ export class IntentController { return res.status(HttpStatus.OK).json(finalResponse); } - @Put('/notice') + @Put('/notice/:id') @ApiBearerAuth() - @Roles(OrgRoles.OWNER) + @Roles(OrgRoles.ECOSYSTEM_LEAD) @UseGuards(AuthGuard('jwt'), EcosystemRolesGuard) @ApiOperation({ summary: 'Update intent notice', - description: 'Updates the notice URL for an intent. The intentId in the body identifies which notice to update.' + description: 'Updates the notice URL for a given notice ID.' }) @ApiResponse({ status: HttpStatus.OK, description: 'Intent notice updated successfully', type: ApiResponseDto }) async updateIntentNotice( + @Param( + 'id', + TrimStringParamPipe, + new ParseUUIDPipe({ + exceptionFactory: (): Error => { + throw new BadRequestException('Invalid notice ID'); + } + }) + ) + id: string, @Body() updateIntentNoticeDto: UpdateIntentNoticeDto, @User() user: PrismaUser, @Res() res: Response ): Promise { - const result = await this.ecosystemService.updateIntentNotice(updateIntentNoticeDto, user); + const result = await this.ecosystemService.updateIntentNotice(id, updateIntentNoticeDto, user); const finalResponse: IResponse = { statusCode: HttpStatus.OK, message: ResponseMessages.intentNotice.success.update, @@ -771,7 +781,7 @@ export class IntentController { @Delete('/notice/:id') @ApiBearerAuth() - @Roles(OrgRoles.OWNER) + @Roles(OrgRoles.ECOSYSTEM_LEAD) @UseGuards(AuthGuard('jwt'), EcosystemRolesGuard) @ApiOperation({ summary: 'Delete intent notice', description: 'Deletes an intent notice by its ID.' }) @ApiResponse({ status: HttpStatus.OK, description: 'Intent notice deleted successfully', type: ApiResponseDto }) diff --git a/apps/api-gateway/src/oid4vc-verification/dtos/create-intent-notice.dto.ts b/apps/api-gateway/src/oid4vc-verification/dtos/create-intent-notice.dto.ts index cd4e7448e..2df30fec1 100644 --- a/apps/api-gateway/src/oid4vc-verification/dtos/create-intent-notice.dto.ts +++ b/apps/api-gateway/src/oid4vc-verification/dtos/create-intent-notice.dto.ts @@ -1,5 +1,5 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { IsDefined, IsOptional, IsUrl, IsUUID } from 'class-validator'; +import { IsDefined, IsOptional, IsString, IsUrl, IsUUID } from 'class-validator'; export class CreateIntentNoticeDto { @ApiProperty({ description: 'Intent ID to associate the notice with', example: 'uuid-of-intent' }) @@ -9,7 +9,7 @@ export class CreateIntentNoticeDto { @ApiProperty({ description: 'URL of the notice', example: 'https://example.com/notice' }) @IsDefined() - @IsUrl() + @IsString() noticeUrl: string; @ApiPropertyOptional({ description: 'Organization ID (optional)', example: 'uuid-of-org' }) @@ -19,11 +19,6 @@ export class CreateIntentNoticeDto { } export class UpdateIntentNoticeDto { - @ApiProperty({ description: 'Intent ID whose notice should be updated', example: 'uuid-of-intent' }) - @IsDefined() - @IsUUID() - intentId: string; - @ApiPropertyOptional({ description: 'URL of the notice', example: 'https://example.com/notice' }) @IsOptional() @IsUrl() diff --git a/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.controller.ts b/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.controller.ts index db9a2b2d0..2f9816c9c 100644 --- a/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.controller.ts +++ b/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.controller.ts @@ -319,6 +319,7 @@ export class Oid4vcVerificationController { description: 'Verification presentation created successfully.', type: ApiResponseDto }) + @ApiQuery({ name: 'ecosystemId', required: false }) @ApiBearerAuth() @Roles(OrgRoles.OWNER) @UseGuards(AuthGuard('jwt'), OrgRolesGuard) @@ -341,19 +342,29 @@ export class Oid4vcVerificationController { }) ) verifierId: string, + @Query( + 'ecosystemId', + new ParseUUIDPipe({ + exceptionFactory: (): Error => { + throw new BadRequestException('Invalid ecosystem ID'); + } + }) + ) + ecosystemId: string, @User() user: user, @Body() createIntentDto: CreateIntentBasedVerificationDto, @Res() res: Response ): Promise { this.logger.debug( - `[createIntentBasedVerificationPresentation] Called with orgId=${orgId}, verifierId=${verifierId}, intent=${createIntentDto?.intent}, user=${user.id}` + `[createIntentBasedVerificationPresentation] Called with orgId=${orgId}, verifierId=${verifierId}, intent=${createIntentDto?.intent}, ecosystemId=${ecosystemId}, user=${user.id}` ); const presentation = await this.oid4vcVerificationService.createIntentBasedVerificationPresentation( orgId, verifierId, createIntentDto, - user + user, + ecosystemId ); this.logger.debug(`[createIntentBasedVerificationPresentation] Presentation created successfully`); diff --git a/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.service.ts b/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.service.ts index 1fe1c4a95..ece7b28f0 100644 --- a/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.service.ts +++ b/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.service.ts @@ -25,10 +25,20 @@ export class Oid4vcVerificationService { orgId: string, verifierId: string, createIntentDto: CreateIntentBasedVerificationDto, - userDetails: user + userDetails: user, + ecosystemId?: string ): Promise { const { intent, responseMode, requestSigner, expectedOrigins } = createIntentDto; - const payload = { orgId, verifierId, intent, responseMode, requestSigner, expectedOrigins, userDetails }; + const payload = { + orgId, + verifierId, + intent, + responseMode, + requestSigner, + expectedOrigins, + userDetails, + ecosystemId + }; this.logger.debug( `[createIntentBasedVerificationPresentation] Called with orgId=${orgId}, verifierId=${verifierId}, intent=${intent}, user=${userDetails?.id}` ); diff --git a/apps/ecosystem/repositories/ecosystem.repository.ts b/apps/ecosystem/repositories/ecosystem.repository.ts index fb382e890..17cf71ca4 100755 --- a/apps/ecosystem/repositories/ecosystem.repository.ts +++ b/apps/ecosystem/repositories/ecosystem.repository.ts @@ -1694,12 +1694,12 @@ export class EcosystemRepository { } // Intent Notice CRUD - async createIntentNotice(intentId: string, noticeUrl: string, userId: string): Promise { + async createIntentNotice(intentId: string, noticeUrl: string, userId: string, orgId?: string): Promise { try { return await this.prisma.intent_notices.create({ data: { - noticeId: intentId, intent: { connect: { id: intentId } }, + ...(orgId && { organisation: { connect: { id: orgId } } }), noticeUrl, createdBy: userId, lastChangedBy: userId @@ -1766,15 +1766,31 @@ export class EcosystemRepository { } } - async getIntentNoticeByIntentId(intentId: string): Promise { + async getIntentNoticeByIntentId(intentId: string, orgId?: string | null): Promise { try { - return await this.prisma.intent_notices.findFirst({ where: { intentId } }); + const where: { intentId: string; orgId?: string | null } = { intentId }; + if (orgId !== undefined) { + where.orgId = orgId; + } + return await this.prisma.intent_notices.findFirst({ where }); } catch (error) { this.logger.error(`getIntentNoticeByIntentId error: ${error}`); throw error; } } + async intentNoticeExists(intentId: string, orgId: string | null): Promise { + try { + const record = await this.prisma.intent_notices.findFirst({ + where: { intentId, orgId: orgId ?? null } + }); + return Boolean(record); + } catch (error) { + this.logger.error(`intentNoticeSlotExists error: ${error}`); + throw error; + } + } + async isEcosystemLead(userId: string, ecosystemId: string): Promise { try { const record = await this.prisma.ecosystem_orgs.findFirst({ @@ -1792,9 +1808,21 @@ export class EcosystemRepository { } } - async updateIntentNotice(intentId: string, noticeUrl: string, userId: string): Promise { + async isUserInOrganisation(userId: string, orgId: string): Promise { try { - const record = await this.prisma.intent_notices.findFirst({ where: { intentId } }); + const record = await this.prisma.user_org_roles.findFirst({ + where: { userId, orgId } + }); + return Boolean(record); + } catch (error) { + this.logger.error(`isUserInOrganisation error: ${error}`); + throw error; + } + } + + async updateIntentNotice(id: string, noticeUrl: string, userId: string): Promise { + try { + const record = await this.prisma.intent_notices.findFirst({ where: { id } }); if (!record) { return null; } diff --git a/apps/ecosystem/src/ecosystem.controller.ts b/apps/ecosystem/src/ecosystem.controller.ts index 0e48eace8..9335fe7cf 100755 --- a/apps/ecosystem/src/ecosystem.controller.ts +++ b/apps/ecosystem/src/ecosystem.controller.ts @@ -358,14 +358,15 @@ export class EcosystemController { // Intent Notice CRUD @MessagePattern({ cmd: 'create-intent-notice' }) async createIntentNotice(payload: { - createIntentNoticeDto: { intentId: string; noticeUrl: string }; + createIntentNoticeDto: { intentId: string; noticeUrl: string; orgId?: string }; userDetails: user; }): Promise { const { createIntentNoticeDto, userDetails } = payload; return this.ecosystemService.createIntentNotice( createIntentNoticeDto.intentId, createIntentNoticeDto.noticeUrl, - userDetails.id + userDetails, + createIntentNoticeDto.orgId ); } @@ -392,17 +393,18 @@ export class EcosystemController { } @MessagePattern({ cmd: 'get-intent-notice-by-intent-id' }) - async getIntentNoticeByIntentId(payload: { intentId: string }): Promise { - return this.ecosystemService.getIntentNoticeByIntentId(payload.intentId); + async getIntentNoticeByIntentId(payload: { intentId: string; orgId?: string | null }): Promise { + return this.ecosystemService.getIntentNoticeByIntentId(payload.intentId, payload.orgId); } @MessagePattern({ cmd: 'update-intent-notice' }) async updateIntentNotice(payload: { - updateIntentNoticeDto: { intentId: string; noticeUrl?: string }; + id: string; + updateIntentNoticeDto: { noticeUrl?: string }; userDetails: user; }): Promise { return this.ecosystemService.updateIntentNotice( - payload.updateIntentNoticeDto.intentId, + payload.id, payload.updateIntentNoticeDto.noticeUrl, payload.userDetails.id ); diff --git a/apps/ecosystem/src/ecosystem.helper.ts b/apps/ecosystem/src/ecosystem.helper.ts new file mode 100644 index 000000000..2c816047e --- /dev/null +++ b/apps/ecosystem/src/ecosystem.helper.ts @@ -0,0 +1,4 @@ +// export function isOrgExistForUser(userDetails: any, orgId: string): boolean { +// const userOrgRoles = userDetails.userOrgRoles as { orgId: string | null }[]; +// return userOrgRoles.some((role) => role.orgId === orgId); +// } diff --git a/apps/ecosystem/src/ecosystem.service.ts b/apps/ecosystem/src/ecosystem.service.ts index c0c53a061..da85a176b 100755 --- a/apps/ecosystem/src/ecosystem.service.ts +++ b/apps/ecosystem/src/ecosystem.service.ts @@ -1018,8 +1018,13 @@ export class EcosystemService { } } - async createIntentNotice(intentId: string, noticeUrl: string, userId: string): Promise { + async createIntentNotice(intentId: string, noticeUrl: string, userDetails: user, orgId?: string): Promise { try { + // if (orgId) { + // if (!isOrgExistForUser(userDetails, orgId)) { + // throw new RpcException({ statusCode: HttpStatus.FORBIDDEN, message: 'Provided orgId does not belong to the user.' }); + // } + // } const intent = await this.ecosystemRepository.findIntentById(intentId); if (!intent) { throw new RpcException({ @@ -1027,8 +1032,16 @@ export class EcosystemService { message: ResponseMessages.intentNotice.error.intentNotFound }); } - await this.validateEcosystemLead(userId, intent['ecosystemId']); - return await this.ecosystemRepository.createIntentNotice(intentId, noticeUrl, userId); + await this.validateEcosystemLead(userDetails.id, intent['ecosystemId']); + const isAlreadyExists = await this.ecosystemRepository.intentNoticeExists(intentId, orgId ?? null); + if (isAlreadyExists) { + const slotLabel = orgId ? `orgId ${orgId}` : 'no orgId'; + throw new RpcException({ + statusCode: HttpStatus.CONFLICT, + message: `An intent notice with ${slotLabel} already exists for this intent.` + }); + } + return await this.ecosystemRepository.createIntentNotice(intentId, noticeUrl, userDetails.id, orgId); } catch (error) { const errorResponse = ErrorHandler.categorize(error, ResponseMessages.intentNotice.error.create); this.logger.error( @@ -1104,9 +1117,9 @@ export class EcosystemService { } } - async getIntentNoticeByIntentId(intentId: string): Promise { + async getIntentNoticeByIntentId(intentId: string, orgId?: string | null): Promise { try { - return await this.ecosystemRepository.getIntentNoticeByIntentId(intentId); + return await this.ecosystemRepository.getIntentNoticeByIntentId(intentId, orgId); } catch (error) { const errorResponse = ErrorHandler.categorize(error, ResponseMessages.intentNotice.error.notFound); this.logger.error( @@ -1117,9 +1130,9 @@ export class EcosystemService { } } - async updateIntentNotice(intentId: string, noticeUrl: string, userId: string): Promise { + async updateIntentNotice(id: string, noticeUrl: string, userId: string): Promise { try { - const updated = await this.ecosystemRepository.updateIntentNotice(intentId, noticeUrl, userId); + const updated = await this.ecosystemRepository.updateIntentNotice(id, noticeUrl, userId); if (!updated) { throw new RpcException({ statusCode: HttpStatus.NOT_FOUND, diff --git a/apps/oid4vc-verification/src/oid4vc-verification.controller.ts b/apps/oid4vc-verification/src/oid4vc-verification.controller.ts index 64eeeb825..506bc4fef 100644 --- a/apps/oid4vc-verification/src/oid4vc-verification.controller.ts +++ b/apps/oid4vc-verification/src/oid4vc-verification.controller.ts @@ -113,8 +113,10 @@ export class Oid4vpVerificationController { requestSigner: IRequestSigner; userDetails: user; expectedOrigins?: string[]; + ecosystemId?: string; }): Promise { - const { orgId, verifierId, intent, responseMode, requestSigner, expectedOrigins, userDetails } = payload; + const { orgId, verifierId, intent, responseMode, requestSigner, expectedOrigins, userDetails, ecosystemId } = + payload; this.logger.debug( `[createIntentBasedVerificationPresentation] Received 'oid4vp-intent-based-verification-presentation' for orgId=${orgId}, verifierId=${verifierId}, intent=${intent}, user=${userDetails?.id ?? 'unknown'}` ); @@ -125,7 +127,8 @@ export class Oid4vpVerificationController { responseMode, requestSigner, userDetails, - expectedOrigins + expectedOrigins, + ecosystemId ); } diff --git a/apps/oid4vc-verification/src/oid4vc-verification.service.ts b/apps/oid4vc-verification/src/oid4vc-verification.service.ts index 511c96d8c..b1837c31d 100644 --- a/apps/oid4vc-verification/src/oid4vc-verification.service.ts +++ b/apps/oid4vc-verification/src/oid4vc-verification.service.ts @@ -313,7 +313,8 @@ export class Oid4vpVerificationService extends BaseService { responseMode: string, requestSigner: IRequestSigner, userDetails: user, - expectedOrigins?: string[] + expectedOrigins?: string[], + ecosystemId?: string ): Promise { this.logger.debug( `[createIntentBasedVerificationPresentation] called for orgId=${orgId}, verifierId=${verifierId}, intent=${intent}, user=${userDetails?.id ?? 'unknown'}` @@ -406,15 +407,21 @@ export class Oid4vpVerificationService extends BaseService { `[createIntentBasedVerificationPresentation] verification presentation created successfully for orgId=${orgId}` ); if (createdSession) { - const intentNotice: any = await this.natsClient - .sendNatsMessage(this.oid4vpVerificationServiceProxy, 'get-intent-notice-by-intent-id', { intentId: intent }) - .catch(() => null); - - if (intentNotice?.noticeUrl) { - createdSession.noticeUrl = intentNotice.noticeUrl; + const intentId: string = templateData?.intentId; + if (intentId) { + const intentNotice: any = await this.natsClient + .sendNatsMessage(this.oid4vpVerificationServiceProxy, 'get-intent-notice-by-intent-id', { + intentId, + orgId + }) + .catch(() => null); + + if (intentNotice?.noticeUrl) { + createdSession.noticeUrl = `${intentNotice.noticeUrl}?transactionId=${createdSession.verificationSession.id}`; + } } } - + console.log('createdSession:::::::::::::::::;;', createdSession); return createdSession; } catch (error) { this.logger.error( diff --git a/libs/prisma-service/prisma/migrations/20260312082536_added_intent_notice_table/migration.sql b/libs/prisma-service/prisma/migrations/20260312082536_added_intent_notice_table/migration.sql new file mode 100644 index 000000000..41457702d --- /dev/null +++ b/libs/prisma-service/prisma/migrations/20260312082536_added_intent_notice_table/migration.sql @@ -0,0 +1,18 @@ +-- CreateTable +CREATE TABLE "intent_notices" ( + "id" UUID NOT NULL, + "intentId" UUID NOT NULL, + "noticeUrl" TEXT NOT NULL, + "createDateTime" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "createdBy" UUID NOT NULL, + "lastChangedDateTime" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "lastChangedBy" TEXT NOT NULL, + + CONSTRAINT "intent_notices_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "intent_notices_intentId_idx" ON "intent_notices"("intentId"); + +-- AddForeignKey +ALTER TABLE "intent_notices" ADD CONSTRAINT "intent_notices_intentId_fkey" FOREIGN KEY ("intentId") REFERENCES "intents"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/libs/prisma-service/prisma/migrations/20260312084927_add_org_id_to_intent_notices/migration.sql b/libs/prisma-service/prisma/migrations/20260312084927_add_org_id_to_intent_notices/migration.sql new file mode 100644 index 000000000..fa9c444f5 --- /dev/null +++ b/libs/prisma-service/prisma/migrations/20260312084927_add_org_id_to_intent_notices/migration.sql @@ -0,0 +1,8 @@ +-- AlterTable +ALTER TABLE "intent_notices" ADD COLUMN "orgId" UUID; + +-- CreateIndex +CREATE INDEX "intent_notices_orgId_idx" ON "intent_notices"("orgId"); + +-- AddForeignKey +ALTER TABLE "intent_notices" ADD CONSTRAINT "intent_notices_orgId_fkey" FOREIGN KEY ("orgId") REFERENCES "organisation"("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 d8f43ccb2..9fa0de723 100755 --- a/libs/prisma-service/prisma/schema.prisma +++ b/libs/prisma-service/prisma/schema.prisma @@ -157,6 +157,7 @@ model organisation { oid4vp_presentations oid4vp_presentations[] verification_templates verification_templates[] intent_templates intent_templates[] + intent_notices intent_notices[] ecosystemOrgs ecosystem_orgs[] ecosystem_invitations ecosystem_invitations[] } @@ -740,16 +741,19 @@ model intent_templates { } model intent_notices { - id String @id @default(uuid()) @db.Uuid - intentId String @db.Uuid + id String @id @default(uuid()) @db.Uuid + intentId String @db.Uuid + orgId String? @db.Uuid noticeUrl String - createDateTime DateTime @default(now()) @db.Timestamptz(6) - createdBy String @db.Uuid - lastChangedDateTime DateTime @default(now()) @db.Timestamptz(6) + createDateTime DateTime @default(now()) @db.Timestamptz(6) + createdBy String @db.Uuid + lastChangedDateTime DateTime @default(now()) @db.Timestamptz(6) lastChangedBy String - intent intents @relation(fields: [intentId], references: [id]) + intent intents @relation(fields: [intentId], references: [id]) + organisation organisation? @relation(fields: [orgId], references: [id]) @@index([intentId]) + @@index([orgId]) } model ecosystem { From f1465487198447129754bb3e15df9fabafe04832 Mon Sep 17 00:00:00 2001 From: Tipu_Singh Date: Tue, 17 Mar 2026 12:20:06 +0530 Subject: [PATCH 4/7] feat: updated the consent notice APIs Signed-off-by: Tipu_Singh --- .../src/ecosystem/ecosystem.service.ts | 8 +-- .../src/ecosystem/intent/intent.controller.ts | 44 +++++---------- .../oid4vc-verification.controller.ts | 2 +- .../oid4vc-verification.service.ts | 2 +- .../repositories/ecosystem.repository.ts | 36 ++++++------ apps/ecosystem/src/ecosystem.controller.ts | 16 +++--- apps/ecosystem/src/ecosystem.helper.ts | 35 ++++++++++-- apps/ecosystem/src/ecosystem.service.ts | 56 +++++++++---------- .../src/oid4vc-verification.controller.ts | 2 +- .../src/oid4vc-verification.service.ts | 4 +- 10 files changed, 107 insertions(+), 98 deletions(-) diff --git a/apps/api-gateway/src/ecosystem/ecosystem.service.ts b/apps/api-gateway/src/ecosystem/ecosystem.service.ts index 46d4ca7ad..03258b2f1 100755 --- a/apps/api-gateway/src/ecosystem/ecosystem.service.ts +++ b/apps/api-gateway/src/ecosystem/ecosystem.service.ts @@ -258,12 +258,8 @@ export class EcosystemService { return this.natsClient.sendNatsMessage(this.serviceProxy, 'create-intent-notice', payload); } - async getIntentNoticeById(id: string): Promise { - return this.natsClient.sendNatsMessage(this.serviceProxy, 'get-intent-notice', { id }); - } - - async getIntentNotices(intentId?: string): Promise { - return this.natsClient.sendNatsMessage(this.serviceProxy, 'get-intent-notices', { intentId }); + async getIntentNotices(id?: string, intentId?: string): Promise { + return this.natsClient.sendNatsMessage(this.serviceProxy, 'get-intent-notices', { id, intentId }); } async getIntentNoticesByEcosystemId( diff --git a/apps/api-gateway/src/ecosystem/intent/intent.controller.ts b/apps/api-gateway/src/ecosystem/intent/intent.controller.ts index fa621c7cf..f03be2a0a 100755 --- a/apps/api-gateway/src/ecosystem/intent/intent.controller.ts +++ b/apps/api-gateway/src/ecosystem/intent/intent.controller.ts @@ -639,12 +639,24 @@ export class IntentController { @UseGuards(AuthGuard('jwt'), EcosystemRolesGuard) @ApiOperation({ summary: 'Get intent notices', - description: 'Retrieves all intent notices, optionally filtered by intentId.' + description: 'Retrieves intent notices. Filter by notice id or intentId (both optional).' }) + @ApiQuery({ name: 'id', required: false, type: String, description: 'Filter by notice PK UUID (optional)' }) @ApiQuery({ name: 'intentId', required: false, type: String, description: 'Filter by intent UUID (optional)' }) @ApiResponse({ status: HttpStatus.OK, description: 'Intent notices fetched successfully', type: ApiResponseDto }) async getIntentNotices( @Res() res: Response, + @Query( + 'id', + new ParseUUIDPipe({ + version: '4', + optional: true, + exceptionFactory: (): Error => { + throw new BadRequestException('Invalid notice ID'); + } + }) + ) + id?: string, @Query( 'intentId', new ParseUUIDPipe({ @@ -657,7 +669,7 @@ export class IntentController { ) intentId?: string ): Promise { - const result = await this.ecosystemService.getIntentNotices(intentId); + const result = await this.ecosystemService.getIntentNotices(id, intentId); const finalResponse: IResponse = { statusCode: HttpStatus.OK, message: ResponseMessages.intentNotice.success.fetchAll, @@ -718,34 +730,6 @@ export class IntentController { }); } - @Get('/notice/:id') - @ApiBearerAuth() - @Roles(OrgRoles.ECOSYSTEM_LEAD, OrgRoles.ECOSYSTEM_MEMBER) - @UseGuards(AuthGuard('jwt'), EcosystemRolesGuard) - @ApiOperation({ summary: 'Get intent notice by ID', description: 'Retrieves a specific intent notice by its ID.' }) - @ApiResponse({ status: HttpStatus.OK, description: 'Intent notice fetched successfully', type: ApiResponseDto }) - async getIntentNoticeById( - @Param( - 'id', - TrimStringParamPipe, - new ParseUUIDPipe({ - exceptionFactory: (): Error => { - throw new BadRequestException('Invalid notice ID'); - } - }) - ) - id: string, - @Res() res: Response - ): Promise { - const result = await this.ecosystemService.getIntentNoticeById(id); - const finalResponse: IResponse = { - statusCode: HttpStatus.OK, - message: ResponseMessages.intentNotice.success.fetch, - data: result - }; - return res.status(HttpStatus.OK).json(finalResponse); - } - @Put('/notice/:id') @ApiBearerAuth() @Roles(OrgRoles.ECOSYSTEM_LEAD) diff --git a/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.controller.ts b/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.controller.ts index 2f9816c9c..620348e98 100644 --- a/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.controller.ts +++ b/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.controller.ts @@ -319,7 +319,7 @@ export class Oid4vcVerificationController { description: 'Verification presentation created successfully.', type: ApiResponseDto }) - @ApiQuery({ name: 'ecosystemId', required: false }) + @ApiQuery({ name: 'ecosystemId', required: true }) @ApiBearerAuth() @Roles(OrgRoles.OWNER) @UseGuards(AuthGuard('jwt'), OrgRolesGuard) diff --git a/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.service.ts b/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.service.ts index ece7b28f0..11d1e37bc 100644 --- a/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.service.ts +++ b/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.service.ts @@ -26,7 +26,7 @@ export class Oid4vcVerificationService { verifierId: string, createIntentDto: CreateIntentBasedVerificationDto, userDetails: user, - ecosystemId?: string + ecosystemId: string ): Promise { const { intent, responseMode, requestSigner, expectedOrigins } = createIntentDto; const payload = { diff --git a/apps/ecosystem/repositories/ecosystem.repository.ts b/apps/ecosystem/repositories/ecosystem.repository.ts index 17cf71ca4..0ed471721 100755 --- a/apps/ecosystem/repositories/ecosystem.repository.ts +++ b/apps/ecosystem/repositories/ecosystem.repository.ts @@ -811,11 +811,15 @@ export class EcosystemRepository { } // eslint-disable-next-line camelcase - async getIntentTemplateByIntentAndOrg(intentName: string, verifierOrgId: string): Promise { + async getIntentTemplateByIntentAndOrg( + intentName: string, + verifierOrgId: string, + ecosystemId?: string + ): Promise { try { const template = await this.prisma.intent_templates.findFirst({ where: { - intent: { is: { name: intentName } }, + intent: { is: { name: intentName, ...(ecosystemId && { ecosystemId }) } }, OR: [{ orgId: verifierOrgId }, { orgId: null }] }, select: { @@ -1711,21 +1715,18 @@ export class EcosystemRepository { } } - async getIntentNoticeById(id: string): Promise { - try { - return await this.prisma.intent_notices.findFirst({ - where: { id } - }); - } catch (error) { - this.logger.error(`getIntentNoticeById error: ${error}`); - throw error; - } - } - - async getIntentNotices(intentId?: string): Promise { + async getIntentNotices(id?: string, intentId?: string): Promise { try { + const where = { + ...(id && { id }), + ...(intentId && { intentId }) + }; return await this.prisma.intent_notices.findMany({ - where: intentId ? { intentId } : {}, + where, + include: { + intent: { select: { id: true, name: true, ecosystemId: true } }, + organisation: { select: { name: true, description: true } } + }, orderBy: { createDateTime: 'desc' } }); } catch (error) { @@ -1751,7 +1752,10 @@ export class EcosystemRepository { const [data, totalCount] = await this.prisma.$transaction([ this.prisma.intent_notices.findMany({ where, - include: { intent: { select: { id: true, name: true, ecosystemId: true } } }, + include: { + intent: { select: { id: true, name: true, ecosystemId: true } }, + organisation: { select: { name: true, description: true } } + }, orderBy: { createDateTime: 'desc' }, skip: (pageNumber - 1) * pageSize, take: pageSize diff --git a/apps/ecosystem/src/ecosystem.controller.ts b/apps/ecosystem/src/ecosystem.controller.ts index 9335fe7cf..19ed790e1 100755 --- a/apps/ecosystem/src/ecosystem.controller.ts +++ b/apps/ecosystem/src/ecosystem.controller.ts @@ -238,8 +238,13 @@ export class EcosystemController { async getIntentTemplateByIntentAndOrg(payload: { intentName: string; verifierOrgId: string; + ecosystemId?: string; }): Promise { - return this.ecosystemService.getIntentTemplateByIntentAndOrg(payload.intentName, payload.verifierOrgId); + return this.ecosystemService.getIntentTemplateByIntentAndOrg( + payload.intentName, + payload.verifierOrgId, + payload.ecosystemId + ); } @MessagePattern({ cmd: 'update-intent-template' }) @@ -370,14 +375,9 @@ export class EcosystemController { ); } - @MessagePattern({ cmd: 'get-intent-notice' }) - async getIntentNoticeById(payload: { id: string }): Promise { - return this.ecosystemService.getIntentNoticeById(payload.id); - } - @MessagePattern({ cmd: 'get-intent-notices' }) - async getIntentNotices(payload: { intentId?: string }): Promise { - return this.ecosystemService.getIntentNotices(payload.intentId); + async getIntentNotices(payload: { id?: string; intentId?: string }): Promise { + return this.ecosystemService.getIntentNotices(payload.id, payload.intentId); } @MessagePattern({ cmd: 'get-intent-notices-by-ecosystem' }) diff --git a/apps/ecosystem/src/ecosystem.helper.ts b/apps/ecosystem/src/ecosystem.helper.ts index 2c816047e..f4746c55f 100644 --- a/apps/ecosystem/src/ecosystem.helper.ts +++ b/apps/ecosystem/src/ecosystem.helper.ts @@ -1,4 +1,31 @@ -// export function isOrgExistForUser(userDetails: any, orgId: string): boolean { -// const userOrgRoles = userDetails.userOrgRoles as { orgId: string | null }[]; -// return userOrgRoles.some((role) => role.orgId === orgId); -// } +import { HttpStatus } from '@nestjs/common'; +import { RpcException } from '@nestjs/microservices'; + +export async function validateNoticeUrl(noticeUrl: string): Promise { + if (!noticeUrl || !noticeUrl.trim()) { + throw new RpcException({ + statusCode: HttpStatus.BAD_REQUEST, + message: 'noticeUrl must not be empty.' + }); + } + try { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 5000); + const response = await fetch(noticeUrl, { method: 'HEAD', signal: controller.signal }); + clearTimeout(timeout); + if (!response.ok) { + throw new RpcException({ + statusCode: HttpStatus.BAD_REQUEST, + message: `noticeUrl is not reachable (HTTP ${response.status}).` + }); + } + } catch (err) { + if (err instanceof RpcException) { + throw err; + } + throw new RpcException({ + statusCode: HttpStatus.BAD_REQUEST, + message: `noticeUrl could not be resolved: ${err?.message ?? 'unreachable'}` + }); + } +} diff --git a/apps/ecosystem/src/ecosystem.service.ts b/apps/ecosystem/src/ecosystem.service.ts index da85a176b..97dc00836 100755 --- a/apps/ecosystem/src/ecosystem.service.ts +++ b/apps/ecosystem/src/ecosystem.service.ts @@ -16,6 +16,7 @@ import { import { ClientProxy, RpcException } from '@nestjs/microservices'; import { ClientRegistrationService } from '@credebl/client-registration'; import { EcosystemRepository } from 'apps/ecosystem/repositories/ecosystem.repository'; +import { validateNoticeUrl } from './ecosystem.helper'; import { CreateEcosystemInviteTemplate } from '../templates/create-ecosystem.templates'; import { EmailDto } from '@credebl/common/dtos/email.dto'; import { InviteMemberToEcosystem } from '../templates/invite-member-template'; @@ -728,9 +729,17 @@ export class EcosystemService { } } - async getIntentTemplateByIntentAndOrg(intentName: string, verifierOrgId: string): Promise { + async getIntentTemplateByIntentAndOrg( + intentName: string, + verifierOrgId: string, + ecosystemId?: string + ): Promise { try { - const intentTemplate = await this.ecosystemRepository.getIntentTemplateByIntentAndOrg(intentName, verifierOrgId); + const intentTemplate = await this.ecosystemRepository.getIntentTemplateByIntentAndOrg( + intentName, + verifierOrgId, + ecosystemId + ); if (!intentTemplate) { this.logger.log( `[getIntentTemplateByIntentAndOrg] - No template found for intent ${intentName} and org ${verifierOrgId}` @@ -1020,12 +1029,10 @@ export class EcosystemService { async createIntentNotice(intentId: string, noticeUrl: string, userDetails: user, orgId?: string): Promise { try { - // if (orgId) { - // if (!isOrgExistForUser(userDetails, orgId)) { - // throw new RpcException({ statusCode: HttpStatus.FORBIDDEN, message: 'Provided orgId does not belong to the user.' }); - // } - // } + await validateNoticeUrl(noticeUrl); + const intent = await this.ecosystemRepository.findIntentById(intentId); + if (!intent) { throw new RpcException({ statusCode: HttpStatus.NOT_FOUND, @@ -1033,6 +1040,17 @@ export class EcosystemService { }); } await this.validateEcosystemLead(userDetails.id, intent['ecosystemId']); + + if (orgId) { + const orgEcosystemMembership = await this.ecosystemRepository.getEcosystemOrg(intent['ecosystemId'], orgId); + if (!orgEcosystemMembership) { + throw new RpcException({ + statusCode: HttpStatus.FORBIDDEN, + message: 'The provided orgId is not a member or lead of this ecosystem.' + }); + } + } + const isAlreadyExists = await this.ecosystemRepository.intentNoticeExists(intentId, orgId ?? null); if (isAlreadyExists) { const slotLabel = orgId ? `orgId ${orgId}` : 'no orgId'; @@ -1052,29 +1070,9 @@ export class EcosystemService { } } - async getIntentNoticeById(id: string): Promise { - try { - const record = await this.ecosystemRepository.getIntentNoticeById(id); - if (!record) { - throw new RpcException({ - statusCode: HttpStatus.NOT_FOUND, - message: ResponseMessages.intentNotice.error.notFound - }); - } - return record; - } catch (error) { - const errorResponse = ErrorHandler.categorize(error, ResponseMessages.intentNotice.error.notFound); - this.logger.error( - `[getIntentNoticeById] ${errorResponse.statusCode}: ${errorResponse.message}`, - ErrorHandler.format(error) - ); - throw new RpcException(errorResponse); - } - } - - async getIntentNotices(intentId?: string): Promise { + async getIntentNotices(id?: string, intentId?: string): Promise { try { - const records = await this.ecosystemRepository.getIntentNotices(intentId); + const records = await this.ecosystemRepository.getIntentNotices(id, intentId); if (!records || 0 === records.length) { throw new RpcException({ statusCode: HttpStatus.NOT_FOUND, diff --git a/apps/oid4vc-verification/src/oid4vc-verification.controller.ts b/apps/oid4vc-verification/src/oid4vc-verification.controller.ts index 506bc4fef..1965dc11a 100644 --- a/apps/oid4vc-verification/src/oid4vc-verification.controller.ts +++ b/apps/oid4vc-verification/src/oid4vc-verification.controller.ts @@ -113,7 +113,7 @@ export class Oid4vpVerificationController { requestSigner: IRequestSigner; userDetails: user; expectedOrigins?: string[]; - ecosystemId?: string; + ecosystemId: string; }): Promise { const { orgId, verifierId, intent, responseMode, requestSigner, expectedOrigins, userDetails, ecosystemId } = payload; diff --git a/apps/oid4vc-verification/src/oid4vc-verification.service.ts b/apps/oid4vc-verification/src/oid4vc-verification.service.ts index b1837c31d..d3b5944a6 100644 --- a/apps/oid4vc-verification/src/oid4vc-verification.service.ts +++ b/apps/oid4vc-verification/src/oid4vc-verification.service.ts @@ -314,7 +314,7 @@ export class Oid4vpVerificationService extends BaseService { requestSigner: IRequestSigner, userDetails: user, expectedOrigins?: string[], - ecosystemId?: string + ecosystemId?: string // kept optional here so existing callers don't break ): Promise { this.logger.debug( `[createIntentBasedVerificationPresentation] called for orgId=${orgId}, verifierId=${verifierId}, intent=${intent}, user=${userDetails?.id ?? 'unknown'}` @@ -340,7 +340,7 @@ export class Oid4vpVerificationService extends BaseService { const templateData = await this.natsClient.sendNatsMessage( this.oid4vpVerificationServiceProxy, 'get-intent-template-by-intent-and-org', - { intentName: intent, verifierOrgId: orgId } + { intentName: intent, verifierOrgId: orgId, ecosystemId } ); if (!templateData) { From e2b0cbecfe9ec31f6eb0ccc5e70bb52b3e32c3ac Mon Sep 17 00:00:00 2001 From: Tipu_Singh Date: Tue, 17 Mar 2026 18:02:34 +0530 Subject: [PATCH 5/7] fix: resolved fetch issue Signed-off-by: Tipu_Singh --- apps/ecosystem/src/ecosystem.helper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/ecosystem/src/ecosystem.helper.ts b/apps/ecosystem/src/ecosystem.helper.ts index f4746c55f..f743d67f8 100644 --- a/apps/ecosystem/src/ecosystem.helper.ts +++ b/apps/ecosystem/src/ecosystem.helper.ts @@ -11,7 +11,7 @@ export async function validateNoticeUrl(noticeUrl: string): Promise { try { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 5000); - const response = await fetch(noticeUrl, { method: 'HEAD', signal: controller.signal }); + const response = await fetch(noticeUrl); clearTimeout(timeout); if (!response.ok) { throw new RpcException({ From 8a2fd10979c3b5159c557ba9ff4fee67c2012c89 Mon Sep 17 00:00:00 2001 From: Tipu_Singh Date: Wed, 18 Mar 2026 17:25:01 +0530 Subject: [PATCH 6/7] refactore: resolved coderabbit suggestions Signed-off-by: Tipu_Singh --- .../src/ecosystem/ecosystem.service.ts | 8 +++--- .../src/ecosystem/intent/intent.controller.ts | 3 ++- .../repositories/ecosystem.repository.ts | 20 +++++++++++--- apps/ecosystem/src/ecosystem.controller.ts | 14 +++++----- apps/ecosystem/src/ecosystem.service.ts | 27 ++++++++++++------- .../src/oid4vc-verification.controller.ts | 4 +-- .../src/oid4vc-verification.helper.ts | 21 +++++++++++++++ .../src/oid4vc-verification.service.ts | 11 +++++--- 8 files changed, 77 insertions(+), 31 deletions(-) create mode 100644 apps/oid4vc-verification/src/oid4vc-verification.helper.ts diff --git a/apps/api-gateway/src/ecosystem/ecosystem.service.ts b/apps/api-gateway/src/ecosystem/ecosystem.service.ts index 03258b2f1..3464f132c 100755 --- a/apps/api-gateway/src/ecosystem/ecosystem.service.ts +++ b/apps/api-gateway/src/ecosystem/ecosystem.service.ts @@ -254,7 +254,7 @@ export class EcosystemService { } async createIntentNotice(createIntentNoticeDto: CreateIntentNoticeDto, userDetails: user): Promise { - const payload = { createIntentNoticeDto, userDetails }; + const payload = { createIntentNoticeDto, userId: userDetails.id }; return this.natsClient.sendNatsMessage(this.serviceProxy, 'create-intent-notice', payload); } @@ -283,11 +283,11 @@ export class EcosystemService { updateIntentNoticeDto: UpdateIntentNoticeDto, userDetails: user ): Promise { - const payload = { id, updateIntentNoticeDto, userDetails }; + const payload = { id, updateIntentNoticeDto, userId: userDetails.id }; return this.natsClient.sendNatsMessage(this.serviceProxy, 'update-intent-notice', payload); } - async deleteIntentNotice(id: string): Promise { - return this.natsClient.sendNatsMessage(this.serviceProxy, 'delete-intent-notice', { id }); + async deleteIntentNotice(id: string, userDetails: user): Promise { + return this.natsClient.sendNatsMessage(this.serviceProxy, 'delete-intent-notice', { id, userId: userDetails.id }); } } diff --git a/apps/api-gateway/src/ecosystem/intent/intent.controller.ts b/apps/api-gateway/src/ecosystem/intent/intent.controller.ts index f03be2a0a..1656b9187 100755 --- a/apps/api-gateway/src/ecosystem/intent/intent.controller.ts +++ b/apps/api-gateway/src/ecosystem/intent/intent.controller.ts @@ -780,9 +780,10 @@ export class IntentController { }) ) id: string, + @User() user: PrismaUser, @Res() res: Response ): Promise { - const result = await this.ecosystemService.deleteIntentNotice(id); + const result = await this.ecosystemService.deleteIntentNotice(id, user); const finalResponse: IResponse = { statusCode: HttpStatus.OK, message: ResponseMessages.intentNotice.success.delete, diff --git a/apps/ecosystem/repositories/ecosystem.repository.ts b/apps/ecosystem/repositories/ecosystem.repository.ts index 0ed471721..43ef98d21 100755 --- a/apps/ecosystem/repositories/ecosystem.repository.ts +++ b/apps/ecosystem/repositories/ecosystem.repository.ts @@ -1717,10 +1717,24 @@ export class EcosystemRepository { async getIntentNotices(id?: string, intentId?: string): Promise { try { - const where = { - ...(id && { id }), - ...(intentId && { intentId }) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const where: any = { + ...(id && { id }) }; + + if (intentId) { + const intent = await this.prisma.intents.findUnique({ + where: { id: intentId }, + select: { ecosystemId: true } + }); + where.intent = { + is: { + id: intentId, + ...(intent && { ecosystemId: intent.ecosystemId }) + } + }; + } + return await this.prisma.intent_notices.findMany({ where, include: { diff --git a/apps/ecosystem/src/ecosystem.controller.ts b/apps/ecosystem/src/ecosystem.controller.ts index 19ed790e1..2e4abf818 100755 --- a/apps/ecosystem/src/ecosystem.controller.ts +++ b/apps/ecosystem/src/ecosystem.controller.ts @@ -364,13 +364,13 @@ export class EcosystemController { @MessagePattern({ cmd: 'create-intent-notice' }) async createIntentNotice(payload: { createIntentNoticeDto: { intentId: string; noticeUrl: string; orgId?: string }; - userDetails: user; + userId: string; }): Promise { - const { createIntentNoticeDto, userDetails } = payload; + const { createIntentNoticeDto, userId } = payload; return this.ecosystemService.createIntentNotice( createIntentNoticeDto.intentId, createIntentNoticeDto.noticeUrl, - userDetails, + userId, createIntentNoticeDto.orgId ); } @@ -401,17 +401,17 @@ export class EcosystemController { async updateIntentNotice(payload: { id: string; updateIntentNoticeDto: { noticeUrl?: string }; - userDetails: user; + userId: string; }): Promise { return this.ecosystemService.updateIntentNotice( payload.id, payload.updateIntentNoticeDto.noticeUrl, - payload.userDetails.id + payload.userId ); } @MessagePattern({ cmd: 'delete-intent-notice' }) - async deleteIntentNotice(payload: { id: string }): Promise { - return this.ecosystemService.deleteIntentNotice(payload.id); + async deleteIntentNotice(payload: { id: string; userId: string }): Promise { + return this.ecosystemService.deleteIntentNotice(payload.id, payload.userId); } } diff --git a/apps/ecosystem/src/ecosystem.service.ts b/apps/ecosystem/src/ecosystem.service.ts index 97dc00836..363720dbb 100755 --- a/apps/ecosystem/src/ecosystem.service.ts +++ b/apps/ecosystem/src/ecosystem.service.ts @@ -1027,7 +1027,7 @@ export class EcosystemService { } } - async createIntentNotice(intentId: string, noticeUrl: string, userDetails: user, orgId?: string): Promise { + async createIntentNotice(intentId: string, noticeUrl: string, userId: string, orgId?: string): Promise { try { await validateNoticeUrl(noticeUrl); @@ -1039,7 +1039,7 @@ export class EcosystemService { message: ResponseMessages.intentNotice.error.intentNotFound }); } - await this.validateEcosystemLead(userDetails.id, intent['ecosystemId']); + await this.validateEcosystemLead(userId, intent['ecosystemId']); if (orgId) { const orgEcosystemMembership = await this.ecosystemRepository.getEcosystemOrg(intent['ecosystemId'], orgId); @@ -1059,7 +1059,7 @@ export class EcosystemService { message: `An intent notice with ${slotLabel} already exists for this intent.` }); } - return await this.ecosystemRepository.createIntentNotice(intentId, noticeUrl, userDetails.id, orgId); + return await this.ecosystemRepository.createIntentNotice(intentId, noticeUrl, userId, orgId); } catch (error) { const errorResponse = ErrorHandler.categorize(error, ResponseMessages.intentNotice.error.create); this.logger.error( @@ -1130,14 +1130,18 @@ export class EcosystemService { async updateIntentNotice(id: string, noticeUrl: string, userId: string): Promise { try { - const updated = await this.ecosystemRepository.updateIntentNotice(id, noticeUrl, userId); - if (!updated) { + await validateNoticeUrl(noticeUrl); + const [notice] = await this.ecosystemRepository.getIntentNotices(id); + if (!notice) { throw new RpcException({ statusCode: HttpStatus.NOT_FOUND, message: ResponseMessages.intentNotice.error.notFound }); } - return updated; + const intent = await this.ecosystemRepository.findIntentById(notice['intentId']); + await this.validateEcosystemLead(userId, intent['ecosystemId']); + + return await this.ecosystemRepository.updateIntentNotice(id, noticeUrl, userId); } catch (error) { const errorResponse = ErrorHandler.categorize(error, ResponseMessages.intentNotice.error.updateFailed); this.logger.error( @@ -1148,16 +1152,19 @@ export class EcosystemService { } } - async deleteIntentNotice(id: string): Promise { + async deleteIntentNotice(id: string, userId: string): Promise { try { - const deleted = await this.ecosystemRepository.deleteIntentNotice(id); - if (!deleted) { + const [notice] = await this.ecosystemRepository.getIntentNotices(id); + if (!notice) { throw new RpcException({ statusCode: HttpStatus.NOT_FOUND, message: ResponseMessages.intentNotice.error.notFound }); } - return deleted; + const intent = await this.ecosystemRepository.findIntentById(notice['intentId']); + await this.validateEcosystemLead(userId, intent['ecosystemId']); + + return await this.ecosystemRepository.deleteIntentNotice(id); } catch (error) { const errorResponse = ErrorHandler.categorize(error, ResponseMessages.intentNotice.error.deleteFailed); this.logger.error( diff --git a/apps/oid4vc-verification/src/oid4vc-verification.controller.ts b/apps/oid4vc-verification/src/oid4vc-verification.controller.ts index 1965dc11a..7354c0d4a 100644 --- a/apps/oid4vc-verification/src/oid4vc-verification.controller.ts +++ b/apps/oid4vc-verification/src/oid4vc-verification.controller.ts @@ -127,8 +127,8 @@ export class Oid4vpVerificationController { responseMode, requestSigner, userDetails, - expectedOrigins, - ecosystemId + ecosystemId, + expectedOrigins ); } diff --git a/apps/oid4vc-verification/src/oid4vc-verification.helper.ts b/apps/oid4vc-verification/src/oid4vc-verification.helper.ts new file mode 100644 index 000000000..368f0289a --- /dev/null +++ b/apps/oid4vc-verification/src/oid4vc-verification.helper.ts @@ -0,0 +1,21 @@ +export async function fetchConsentNotice(noticeUrl: string, transactionId: string): Promise { + if (!noticeUrl?.trim() || !transactionId?.trim()) { + throw new Error('noticeUrl and transactionId are required and must not be empty.'); + } + + const consentNoticeUrl = `${noticeUrl}?transactionId=${transactionId}`; + + const response = await fetch(consentNoticeUrl); + + if (!response.ok) { + throw new Error(`consentNoticeUrl is not reachable (HTTP ${response.status}).`); + } + + const data = await response.json(); + + if (!data?.consentNoticeUrl) { + throw new Error('consentNoticeUrl is missing in the consent notice response.'); + } + + return data.consentNoticeUrl; +} diff --git a/apps/oid4vc-verification/src/oid4vc-verification.service.ts b/apps/oid4vc-verification/src/oid4vc-verification.service.ts index d3b5944a6..3daea4f1e 100644 --- a/apps/oid4vc-verification/src/oid4vc-verification.service.ts +++ b/apps/oid4vc-verification/src/oid4vc-verification.service.ts @@ -5,6 +5,7 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/naming-convention, @typescript-eslint/explicit-function-return-type, @typescript-eslint/explicit-module-boundary-types, camelcase */ +import { fetchConsentNotice } from './oid4vc-verification.helper'; import { BadRequestException, @@ -313,8 +314,8 @@ export class Oid4vpVerificationService extends BaseService { responseMode: string, requestSigner: IRequestSigner, userDetails: user, - expectedOrigins?: string[], - ecosystemId?: string // kept optional here so existing callers don't break + ecosystemId: string, + expectedOrigins?: string[] ): Promise { this.logger.debug( `[createIntentBasedVerificationPresentation] called for orgId=${orgId}, verifierId=${verifierId}, intent=${intent}, user=${userDetails?.id ?? 'unknown'}` @@ -417,11 +418,13 @@ export class Oid4vpVerificationService extends BaseService { .catch(() => null); if (intentNotice?.noticeUrl) { - createdSession.noticeUrl = `${intentNotice.noticeUrl}?transactionId=${createdSession.verificationSession.id}`; + createdSession.consentNoticeUrl = await fetchConsentNotice( + intentNotice.noticeUrl, + createdSession.verificationSession.id + ); } } } - console.log('createdSession:::::::::::::::::;;', createdSession); return createdSession; } catch (error) { this.logger.error( From 5903dec7fb334206583c2c9bb1a2a9482cdb6b18 Mon Sep 17 00:00:00 2001 From: Tipu_Singh Date: Wed, 18 Mar 2026 17:51:36 +0530 Subject: [PATCH 7/7] refactor: resolved intent based verification presentation comments Signed-off-by: Tipu_Singh --- apps/ecosystem/src/ecosystem.helper.ts | 3 --- .../oid4vc-verification/src/oid4vc-verification.service.ts | 7 ++++++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/apps/ecosystem/src/ecosystem.helper.ts b/apps/ecosystem/src/ecosystem.helper.ts index f743d67f8..ba3e5adb9 100644 --- a/apps/ecosystem/src/ecosystem.helper.ts +++ b/apps/ecosystem/src/ecosystem.helper.ts @@ -9,10 +9,7 @@ export async function validateNoticeUrl(noticeUrl: string): Promise { }); } try { - const controller = new AbortController(); - const timeout = setTimeout(() => controller.abort(), 5000); const response = await fetch(noticeUrl); - clearTimeout(timeout); if (!response.ok) { throw new RpcException({ statusCode: HttpStatus.BAD_REQUEST, diff --git a/apps/oid4vc-verification/src/oid4vc-verification.service.ts b/apps/oid4vc-verification/src/oid4vc-verification.service.ts index 3daea4f1e..060d8365c 100644 --- a/apps/oid4vc-verification/src/oid4vc-verification.service.ts +++ b/apps/oid4vc-verification/src/oid4vc-verification.service.ts @@ -421,7 +421,12 @@ export class Oid4vpVerificationService extends BaseService { createdSession.consentNoticeUrl = await fetchConsentNotice( intentNotice.noticeUrl, createdSession.verificationSession.id - ); + ).catch((err) => { + this.logger.warn( + `[createIntentBasedVerificationPresentation] consent notice enrichment failed: ${err?.message}` + ); + return null; + }); } } }