diff --git a/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer.dto.ts b/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer.dto.ts index 74eafc674..f3e670072 100644 --- a/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer.dto.ts +++ b/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer.dto.ts @@ -1,6 +1,6 @@ /* eslint-disable camelcase */ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { IsString, IsOptional, IsArray, ValidateNested, IsUrl, IsInt, Min } from 'class-validator'; +import { IsString, IsOptional, IsArray, ValidateNested, IsUrl, IsInt, Min, IsBoolean } from 'class-validator'; import { Type } from 'class-transformer'; export class LogoDto { @@ -178,4 +178,12 @@ export class IssuerUpdationDto { @IsOptional() @IsInt({ message: 'batchCredentialIssuanceSize must be an integer' }) batchCredentialIssuanceSize?: number; + + @ApiPropertyOptional({ + description: 'Marks whether this issuer is the primary issuer', + example: true + }) + @IsOptional() + @IsBoolean() + isPrimary?: boolean; } diff --git a/apps/oid4vc-issuance/interfaces/oid4vc-issuance.interfaces.ts b/apps/oid4vc-issuance/interfaces/oid4vc-issuance.interfaces.ts index be9613ed6..9cec8bb15 100644 --- a/apps/oid4vc-issuance/interfaces/oid4vc-issuance.interfaces.ts +++ b/apps/oid4vc-issuance/interfaces/oid4vc-issuance.interfaces.ts @@ -79,6 +79,8 @@ export interface IssuerMetadata { createdById: string; orgAgentId: string; batchCredentialIssuanceSize?: number; + isPrimary: boolean; + orgId: string; } export interface initialIssuerDetails { @@ -95,6 +97,8 @@ export interface IssuerUpdation { accessTokenSignerKeyType: AccessTokenSignerKeyType; display; batchCredentialIssuanceSize?: number; + isPrimary?: boolean; + orgId: string; } export interface IAgentNatsPayload { diff --git a/apps/oid4vc-issuance/src/oid4vc-issuance.repository.ts b/apps/oid4vc-issuance/src/oid4vc-issuance.repository.ts index b90a25abf..9baa95120 100644 --- a/apps/oid4vc-issuance/src/oid4vc-issuance.repository.ts +++ b/apps/oid4vc-issuance/src/oid4vc-issuance.repository.ts @@ -1,11 +1,12 @@ /* eslint-disable camelcase */ -import { Injectable, Logger, NotFoundException } from '@nestjs/common'; +import { BadRequestException, Injectable, Logger, NotFoundException } from '@nestjs/common'; +import { IssuerMetadata, IssuerUpdation, OrgAgent } from '../interfaces/oid4vc-issuance.interfaces'; // eslint-disable-next-line camelcase import { Prisma, credential_templates, oidc_issuer, org_agents } from '@prisma/client'; +import { x5cKeyType, x5cRecordStatus } from '@credebl/enum/enum'; + import { PrismaService } from '@credebl/prisma-service'; -import { IssuerMetadata, IssuerUpdation, OrgAgent } from '../interfaces/oid4vc-issuance.interfaces'; import { ResponseMessages } from '@credebl/common/response-messages'; -import { x5cKeyType, x5cRecordStatus } from '@credebl/enum/enum'; import { X509CertificateRecord } from '@credebl/common/interfaces/x509.interface'; @Injectable() @@ -190,8 +191,15 @@ export class Oid4vcIssuanceRepository { // eslint-disable-next-line @typescript-eslint/no-explicit-any async addOidcIssuerDetails(issuerMetadata: IssuerMetadata, issuerProfileJson): Promise { try { - const { publicIssuerId, createdById, orgAgentId, batchCredentialIssuanceSize, authorizationServerUrl } = - issuerMetadata; + const { + publicIssuerId, + createdById, + orgAgentId, + batchCredentialIssuanceSize, + authorizationServerUrl, + isPrimary, + orgId + } = issuerMetadata; const oidcIssuerDetails = await this.prisma.oidc_issuer.create({ data: { metadata: issuerProfileJson, @@ -199,7 +207,9 @@ export class Oid4vcIssuanceRepository { createdBy: createdById, orgAgentId, batchCredentialIssuanceSize, - authorizationServerUrl + authorizationServerUrl, + isPrimary, + orgId } }); @@ -210,21 +220,71 @@ export class Oid4vcIssuanceRepository { } } - async updateOidcIssuerDetails(createdById: string, issuerConfig: IssuerUpdation): Promise { + async hasPrimaryIssuer(orgId: string): Promise { try { - const { issuerId, display, batchCredentialIssuanceSize } = issuerConfig; - const oidcIssuerDetails = await this.prisma.oidc_issuer.update({ - where: { id: issuerId }, - data: { - metadata: display as unknown as Prisma.InputJsonValue, - createdBy: createdById, - ...(batchCredentialIssuanceSize !== undefined ? { batchCredentialIssuanceSize } : {}) + const count = await this.prisma.oidc_issuer.count({ + where: { + orgId, + isPrimary: true } }); + return 0 < count; + } catch (error) { + this.logger.error(`[hasPrimaryIssuer] - error: ${JSON.stringify(error)}`); + throw error; + } + } - return oidcIssuerDetails; + async updateOidcIssuerDetails(createdById: string, issuerConfig: IssuerUpdation): Promise { + try { + const { issuerId, display, batchCredentialIssuanceSize, isPrimary, orgId } = issuerConfig; + + return await this.prisma.$transaction(async (tx) => { + const issuer = await tx.oidc_issuer.findFirst({ + where: { + id: issuerId, + orgId + } + }); + + if (!issuer) { + throw new NotFoundException(ResponseMessages.oidcIssuer.error.notFound); + } + + if (isPrimary) { + await tx.oidc_issuer.updateMany({ + where: { orgId, isPrimary: true }, + data: { isPrimary: false } + }); + } else { + const otherPrimaryExists = await tx.oidc_issuer.count({ + where: { + orgId, + isPrimary: true, + NOT: { id: issuerId } + } + }); + + if (0 === otherPrimaryExists) { + throw new BadRequestException(ResponseMessages.oidcIssuer.error.setPrimaryIssuerFailed); + } + } + const updatedIssuer = await tx.oidc_issuer.update({ + where: { id: issuerId }, + data: { + metadata: display as unknown as Prisma.InputJsonValue, + createdBy: createdById, + ...(batchCredentialIssuanceSize !== undefined && { + batchCredentialIssuanceSize + }), + ...(isPrimary !== undefined && { isPrimary }) + } + }); + + return updatedIssuer; + }); } catch (error) { - this.logger.error(`[addOidcIssuerDetails] - error: ${JSON.stringify(error)}`); + this.logger.error(`[updateOidcIssuerDetails] - error: ${JSON.stringify(error)}`); throw error; } } diff --git a/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts b/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts index 811da6aa1..859373257 100644 --- a/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts +++ b/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts @@ -94,6 +94,11 @@ export class Oid4vcIssuanceService { throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); } const { agentEndPoint, id: orgAgentId, orgAgentTypeId } = agentDetails; + + const hasPrimary = await this.oid4vcIssuanceRepository.hasPrimaryIssuer(orgId); + + const isPrimary = !hasPrimary; + const orgAgentType = await this.oid4vcIssuanceRepository.getOrgAgentType(orgAgentTypeId); if (!orgAgentType) { throw new NotFoundException(ResponseMessages.issuance.error.orgAgentTypeNotFound); @@ -138,7 +143,9 @@ export class Oid4vcIssuanceService { publicIssuerId: issuerIdFromAgent, createdById: userDetails.id, orgAgentId, - batchCredentialIssuanceSize: issuerCreation?.batchCredentialIssuanceSize + batchCredentialIssuanceSize: issuerCreation?.batchCredentialIssuanceSize, + isPrimary, + orgId }; const addOidcIssuerDetails = await this.oid4vcIssuanceRepository.addOidcIssuerDetails( issuerMetadata, @@ -157,10 +164,10 @@ export class Oid4vcIssuanceService { async oidcIssuerUpdate(issuerUpdationConfig: IssuerUpdation, orgId: string, userDetails: user): Promise { try { - const getIssuerDetails = await this.oid4vcIssuanceRepository.getOidcIssuerDetailsById( + const existingIssuer = await this.oid4vcIssuanceRepository.getOidcIssuerDetailsById( issuerUpdationConfig.issuerId ); - if (!getIssuerDetails) { + if (!existingIssuer) { throw new NotFoundException(ResponseMessages.oidcIssuer.error.notFound); } const agentDetails = await this.oid4vcIssuanceRepository.getAgentEndPoint(orgId); @@ -182,7 +189,7 @@ export class Oid4vcIssuanceService { throw new InternalServerErrorException('Error in updating OID4VC Issuer details in DB'); } - const url = getAgentUrl(agentEndPoint, CommonConstants.OIDC_ISSUER_TEMPLATE, getIssuerDetails.publicIssuerId); + const url = getAgentUrl(agentEndPoint, CommonConstants.OIDC_ISSUER_TEMPLATE, existingIssuer.publicIssuerId); const issuerConfig = await this.buildOidcIssuerConfig(issuerUpdationConfig.issuerId); const updatedIssuer = await this._createOIDCTemplate(issuerConfig, url, orgId); if (updatedIssuer?.response?.statusCode && 200 !== updatedIssuer?.response?.statusCode) { diff --git a/libs/common/src/response-messages/index.ts b/libs/common/src/response-messages/index.ts index efee0dc53..a535d3989 100644 --- a/libs/common/src/response-messages/index.ts +++ b/libs/common/src/response-messages/index.ts @@ -671,7 +671,8 @@ export const ResponseMessages = { invalidId: 'Invalid OID4VC issuer ID.', createFailed: 'Failed to create OID4VC issuer.', updateFailed: 'Failed to update OID4VC issuer.', - deleteFailed: 'Failed to delete OID4VC issuer.' + deleteFailed: 'Failed to delete OID4VC issuer.', + setPrimaryIssuerFailed: 'Cannot unset primary. Please assign another issuer as primary first.' } }, oidcTemplate: { diff --git a/libs/prisma-service/prisma/migrations/20260427080006_added_org_id_and_is_primary_in_oidc_issuer/migration.sql b/libs/prisma-service/prisma/migrations/20260427080006_added_org_id_and_is_primary_in_oidc_issuer/migration.sql new file mode 100644 index 000000000..20aaea3c4 --- /dev/null +++ b/libs/prisma-service/prisma/migrations/20260427080006_added_org_id_and_is_primary_in_oidc_issuer/migration.sql @@ -0,0 +1,21 @@ +-- DropForeignKey +ALTER TABLE "issued_oid4vc_credentials" DROP CONSTRAINT "issued_oid4vc_credentials_orgId_fkey"; + +-- DropForeignKey +ALTER TABLE "oid4vc_credentials" DROP CONSTRAINT "oid4vc_credentials_orgId_fkey"; + +-- AlterTable +ALTER TABLE "oid4vc_credentials" ALTER COLUMN "orgId" DROP NOT NULL; + +-- AlterTable +ALTER TABLE "oidc_issuer" ADD COLUMN "isPrimary" BOOLEAN NOT NULL DEFAULT false, +ADD COLUMN "orgId" UUID; + +-- AddForeignKey +ALTER TABLE "oidc_issuer" ADD CONSTRAINT "oidc_issuer_orgId_fkey" FOREIGN KEY ("orgId") REFERENCES "organisation"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "oid4vc_credentials" ADD CONSTRAINT "oid4vc_credentials_orgId_fkey" FOREIGN KEY ("orgId") REFERENCES "organisation"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "issued_oid4vc_credentials" ADD CONSTRAINT "issued_oid4vc_credentials_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 2ef2327c5..8e5766d27 100755 --- a/libs/prisma-service/prisma/schema.prisma +++ b/libs/prisma-service/prisma/schema.prisma @@ -162,6 +162,7 @@ model organisation { ecosystem_invitations ecosystem_invitations[] status_list_allocations status_list_allocation[] issued_oid4vc_credentials issued_oid4vc_credentials[] + oidc_issuer oidc_issuer[] } model org_invitations { @@ -596,6 +597,11 @@ model oidc_issuer { templates credential_templates[] batchCredentialIssuanceSize Int @default(0) + orgId String? @db.Uuid + isPrimary Boolean @default(false) + + organisation organisation? @relation(fields: [orgId], references: [id]) + @@index([orgAgentId]) }