Skip to content

Commit

Permalink
feat: add W3C credential issuance (#555)
Browse files Browse the repository at this point in the history
* feat:added w3c issuance

Signed-off-by: pallavicoder <[email protected]>

* feat:added jsonld credentail issuance

Signed-off-by: pallavicoder <[email protected]>

* feat:added W3C issuance

Signed-off-by: pallavicoder <[email protected]>

---------

Signed-off-by: pallavicoder <[email protected]>
  • Loading branch information
pallavighule authored Feb 29, 2024
1 parent 4bad987 commit 7e36041
Show file tree
Hide file tree
Showing 8 changed files with 396 additions and 58 deletions.
193 changes: 187 additions & 6 deletions apps/api-gateway/src/issuance/dtos/issuance.dto.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,109 @@
/* eslint-disable @typescript-eslint/array-type */

import { IsArray, IsNotEmpty, IsOptional, IsString, IsEmail, ArrayMaxSize, ValidateNested, ArrayMinSize, IsBoolean, IsDefined, MaxLength, IsEnum } from 'class-validator';
import { IsArray, IsNotEmpty, IsOptional, IsString, IsEmail, ArrayMaxSize, ValidateNested, ArrayMinSize, IsBoolean, IsDefined, MaxLength, IsEnum, IsObject} from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Transform, Type } from 'class-transformer';
import { trim } from '@credebl/common/cast.helper';
import { SortValue } from '../../enum';
import { SortFields } from 'apps/connection/src/enum/connection.enum';
import { AutoAccept } from '@credebl/enum/enum';
import { IssueCredentialType, JsonLdCredentialDetailCredentialStatusOptions, JsonLdCredentialDetailOptionsOptions, JsonObject } from '../interfaces';
import { IsCredentialJsonLdContext, SingleOrArray } from '../utils/helper';

class Issuer {
@ApiProperty()
@IsNotEmpty({ message: 'id is required' })
@Type(() => String)
id:string | { id?: string };
}
class Credential {
@ApiProperty()
@IsNotEmpty({ message: 'context is required' })
@IsCredentialJsonLdContext()
'@context': Array<string | JsonObject>;

@ApiProperty()
@IsNotEmpty({ message: 'type is required' })
type: string[];

@ApiProperty()
@IsString({ message: 'id should be string' })
@IsNotEmpty({ message: 'id is required' })
@Type(() => String)
@IsOptional()
id?:string;


@ApiProperty()
@ValidateNested({ each: true })
@Type(() => Issuer)
issuer:Issuer;

@ApiProperty()
@IsString({ message: 'issuance date should be string' })
@IsNotEmpty({ message: 'issuance date is required' })
@Type(() => String)
issuanceDate:string;

@ApiProperty()
@IsString({ message: 'expiration date should be string' })
@IsNotEmpty({ message: 'expiration date is required' })
@Type(() => String)
@IsOptional()
expirationDate?:string;

@ApiProperty()
@IsNotEmpty({ message: ' credential subject required' })
credentialSubject: SingleOrArray<JsonObject>;
[key: string]: unknown

}

export class JsonLdCredentialDetailCredentialStatus {
public constructor(options: JsonLdCredentialDetailCredentialStatusOptions) {
if (options) {
this.type = options.type;
}
}
@IsString()
public type!: string;
}
export class JsonLdCredentialDetailOptions {
public constructor(options: JsonLdCredentialDetailOptionsOptions) {
if (options) {
this.proofPurpose = options.proofPurpose;
this.created = options.created;
this.domain = options.domain;
this.challenge = options.challenge;
this.credentialStatus = options.credentialStatus;
this.proofType = options.proofType;
}
}

@IsString()
@IsNotEmpty({ message: 'proof purpose is required' })
public proofPurpose!: string;

@IsString()
@IsOptional()
public created?: string;

@IsString()
@IsOptional()
public domain?: string;

@IsString()
@IsOptional()
public challenge?: string;

@IsString()
@IsNotEmpty({ message: 'proof type is required' })
public proofType!: string;

@IsOptional()
@IsObject()
public credentialStatus?: JsonLdCredentialDetailCredentialStatus;
}
class Attribute {
@ApiProperty()
@IsString({ message: 'Attribute name should be string' })
Expand All @@ -33,7 +130,8 @@ class CredentialsIssuanceDto {
@IsNotEmpty({ message: 'Please provide valid credential definition id' })
@IsString({ message: 'credential definition id should be string' })
@Transform(({ value }) => value.trim())
credentialDefinitionId: string;
@IsOptional()
credentialDefinitionId?: string;

@ApiProperty({ example: 'string' })
@IsNotEmpty({ message: 'Please provide valid comment' })
Expand Down Expand Up @@ -86,6 +184,12 @@ class CredentialsIssuanceDto {
})
autoAcceptCredential?: string;

@ApiProperty({ example: 'jsonld' })
@IsNotEmpty({ message: 'Please provide credential type ' })
@Transform(({ value }) => trim(value).toLocaleLowerCase())
@IsOptional()
credentialType:IssueCredentialType;

orgId: string;
}

Expand All @@ -101,8 +205,29 @@ export class OOBIssueCredentialDto extends CredentialsIssuanceDto {
@IsArray()
@ValidateNested({ each: true })
@ArrayMinSize(1)
@IsOptional()
@IsNotEmpty({ message: 'Please provide valid attributes' })
@Type(() => Attribute)
attributes: Attribute[];
attributes?: Attribute[];


@ApiProperty()
@IsNotEmpty({ message: 'Please provide valid credential' })
@IsObject({ message: 'credential should be an object' })
@Type(() => Credential)
@IsOptional()
@ValidateNested({ each: true })
credential?:Credential;


@ApiProperty()
@IsOptional()
@IsNotEmpty({ message: 'Please provide valid options' })
@IsObject({ message: 'options should be an object' })
@ValidateNested({ each: true })
@Type(() => JsonLdCredentialDetailOptions)
options?:JsonLdCredentialDetailOptions;

}

class CredentialOffer {
Expand All @@ -111,7 +236,8 @@ class CredentialOffer {
@IsArray({ message: 'Attributes should be an array' })
@ValidateNested({ each: true })
@Type(() => Attribute)
attributes: Attribute[];
@IsOptional()
attributes?: Attribute[];

@ApiProperty({ example: '[email protected]' })
@IsEmail({}, { message: 'Please provide a valid email' })
Expand All @@ -121,6 +247,22 @@ class CredentialOffer {
@Transform(({ value }) => trim(value))
@Type(() => String)
emailId: string;

@IsNotEmpty({ message: 'Please provide valid credential' })
@IsObject({ message: 'credential should be an object' })
@Type(() => Credential)
@IsOptional()
@ValidateNested({ each: true })
credential?:Credential;

@ApiProperty()
@IsOptional()
@IsNotEmpty({ message: 'Please provide valid options' })
@IsObject({ message: 'options should be an object' })
@ValidateNested({ each: true })
@Type(() => JsonLdCredentialDetailOptions)
options?:JsonLdCredentialDetailOptions;

}

export class IssueCredentialDto extends OOBIssueCredentialDto {
Expand Down Expand Up @@ -218,7 +360,39 @@ export class CredentialAttributes {
}

export class OOBCredentialDtoWithEmail {
@ApiProperty({ example: [{ 'emailId': '[email protected]', 'attributes': [{ 'value': 'string', 'name': 'string' }] }] })
@ApiProperty({ example: [
{
'emailId': '[email protected]',
'credential': {
'@context': [
'https://www.w3.org/2018/credentials/v1',
'https://www.w3.org/2018/credentials/examples/v1'
],
'type': [
'VerifiableCredential',
'UniversityDegreeCredential'
],
'issuer': {
'id': 'did:key:z6Mkn72LVp3mq1fWSefkSMh5V7qrmGfCV4KH3K6SoTM21ouM'
},
'issuanceDate': '2019-10-12T07:20:50.52Z',
'credentialSubject': {
'id': 'did:key:z6Mkn72LVp3mq1fWSefkSMh5V7qrmGfCV4KH3K6SoTM21ouM',
'degree': {
'type': 'BachelorDegree',
'name': 'Bachelor of Science and Arts'
}
}
},
'options': {
'proofType': 'Ed25519Signature2018',
'proofPurpose': 'assertionMethod'
}
}
]


})
@IsNotEmpty({ message: 'Please provide valid attributes' })
@IsArray({ message: 'attributes should be array' })
@ArrayMaxSize(Number(process.env.OOB_BATCH_SIZE), { message: `Limit reached (${process.env.OOB_BATCH_SIZE} credentials max). Easily handle larger batches via seamless CSV file uploads` })
Expand All @@ -229,8 +403,9 @@ export class OOBCredentialDtoWithEmail {
@ApiProperty({ example: 'string' })
@IsNotEmpty({ message: 'Please provide valid credential definition id' })
@IsString({ message: 'credential definition id should be string' })
@IsOptional()
@Transform(({ value }) => value.trim())
credentialDefinitionId: string;
credentialDefinitionId?: string;

@ApiProperty({ example: 'string' })
@IsOptional()
Expand All @@ -244,6 +419,12 @@ export class OOBCredentialDtoWithEmail {
@IsString({ message: 'protocol version should be string' })
protocolVersion?: string;

@ApiProperty({ example: 'jsonld' })
@IsNotEmpty({ message: 'Please provide credential type ' })
@Transform(({ value }) => trim(value).toLocaleLowerCase())
@IsOptional()
credentialType:IssueCredentialType;

imageUrl?: string;

orgId: string;
Expand Down
24 changes: 24 additions & 0 deletions apps/api-gateway/src/issuance/interfaces/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { JsonLdCredentialDetailCredentialStatus } from '../dtos/issuance.dto';
import { JsonValue } from '../utils/helper';

export interface IUserRequestInterface {
userId: string;
email: string;
Expand Down Expand Up @@ -75,3 +78,24 @@ export interface IIssuedCredentialSearchParams {
searchByText: string;
}

export enum IssueCredentialType {
JSONLD = 'jsonld',
INDY = 'indy'
}

export interface JsonObject {
[property: string]: JsonValue
}

export interface JsonLdCredentialDetailCredentialStatusOptions {
type: string
}

export interface JsonLdCredentialDetailOptionsOptions {
proofPurpose: string
created?: string
domain?: string
challenge?: string
credentialStatus?: JsonLdCredentialDetailCredentialStatus
proofType: string
}
29 changes: 27 additions & 2 deletions apps/api-gateway/src/issuance/issuance.controller.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable default-param-last */
/* eslint-disable no-param-reassign */
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable camelcase */
Expand All @@ -15,7 +16,9 @@ import {
Header,
UploadedFile,
UseInterceptors,
Logger
Logger,
BadRequestException,
NotFoundException
} from '@nestjs/common';
import {
ApiTags,
Expand Down Expand Up @@ -52,7 +55,7 @@ import { Roles } from '../authz/decorators/roles.decorator';
import { OrgRoles } from 'libs/org-roles/enums';
import { OrgRolesGuard } from '../authz/guards/org-roles.guard';
import { CustomExceptionFilter } from 'apps/api-gateway/common/exception-handler';
import { FileExportResponse, IIssuedCredentialSearchParams, RequestPayload } from './interfaces';
import { FileExportResponse, IIssuedCredentialSearchParams, IssueCredentialType, RequestPayload } from './interfaces';
import { AwsService } from '@credebl/aws';
import { FileInterceptor } from '@nestjs/platform-express';
import { v4 as uuidv4 } from 'uuid';
Expand Down Expand Up @@ -536,14 +539,30 @@ export class IssuanceController {
@ApiBearerAuth()
@UseGuards(AuthGuard('jwt'), OrgRolesGuard)
@Roles(OrgRoles.OWNER, OrgRoles.ADMIN, OrgRoles.ISSUER)
@ApiQuery({
name:'credentialType',
enum: IssueCredentialType
})
async outOfBandCredentialOffer(
@User() user: IUserRequest,
@Body() outOfBandCredentialDto: OOBCredentialDtoWithEmail,
@Query('credentialType') credentialType: IssueCredentialType = IssueCredentialType.INDY,
@Param('orgId') orgId: string,
@Res() res: Response
): Promise<Response> {
outOfBandCredentialDto.orgId = orgId;
outOfBandCredentialDto.credentialType = credentialType;
const credOffer = outOfBandCredentialDto?.credentialOffer || [];
if (IssueCredentialType.INDY !== credentialType && IssueCredentialType.JSONLD !== credentialType) {
throw new NotFoundException(ResponseMessages.issuance.error.invalidCredentialType);
}
if (outOfBandCredentialDto.credentialType === IssueCredentialType.JSONLD && credOffer.every(offer => (!offer?.credential || 0 === Object.keys(offer?.credential).length))) {
throw new BadRequestException(ResponseMessages.issuance.error.credentialNotPresent);
}

if (outOfBandCredentialDto.credentialType === IssueCredentialType.JSONLD && credOffer.every(offer => (!offer?.options || 0 === Object.keys(offer?.options).length))) {
throw new BadRequestException(ResponseMessages.issuance.error.optionsNotPresent);
}
const getCredentialDetails = await this.issueCredentialService.outOfBandCredentialOffer(
user,
outOfBandCredentialDto
Expand All @@ -568,15 +587,21 @@ export class IssuanceController {
summary: `Create out-of-band credential offer`,
description: `Creates an out-of-band credential offer`
})
@ApiQuery({
name:'credentialType',
enum: IssueCredentialType
})
@UseGuards(AuthGuard('jwt'), OrgRolesGuard)
@Roles(OrgRoles.OWNER, OrgRoles.ADMIN, OrgRoles.ISSUER)
@ApiResponse({ status: HttpStatus.CREATED, description: 'Success', type: ApiResponseDto })
async createOOBCredentialOffer(
@Query('credentialType') credentialType: IssueCredentialType = IssueCredentialType.INDY,
@Param('orgId') orgId: string,
@Body() issueCredentialDto: OOBIssueCredentialDto,
@Res() res: Response
): Promise<Response> {
issueCredentialDto.orgId = orgId;
issueCredentialDto.credentialType = credentialType;
const getCredentialDetails = await this.issueCredentialService.sendCredentialOutOfBand(issueCredentialDto);
const finalResponse: IResponseType = {
statusCode: HttpStatus.CREATED,
Expand Down
11 changes: 9 additions & 2 deletions apps/api-gateway/src/issuance/issuance.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { ClientProxy } from '@nestjs/microservices';
import { BaseService } from 'libs/service/base.service';
import { IUserRequest } from '@credebl/user-request/user-request.interface';
import { ClientDetails, FileParameter, IssuanceDto, IssueCredentialDto, OOBCredentialDtoWithEmail, OOBIssueCredentialDto, PreviewFileDetails } from './dtos/issuance.dto';
import { FileExportResponse, IIssuedCredentialSearchParams, RequestPayload } from './interfaces';
import { FileExportResponse, IIssuedCredentialSearchParams, IssueCredentialType, RequestPayload } from './interfaces';
import { IIssuedCredential } from '@credebl/common/interfaces/issuance.interface';

@Injectable()
Expand All @@ -29,7 +29,14 @@ export class IssuanceService extends BaseService {
sendCredentialOutOfBand(issueCredentialDto: OOBIssueCredentialDto): Promise<{
response: object;
}> {
const payload = { attributes: issueCredentialDto.attributes, comment: issueCredentialDto.comment, credentialDefinitionId: issueCredentialDto.credentialDefinitionId, orgId: issueCredentialDto.orgId, protocolVersion: issueCredentialDto.protocolVersion, goalCode: issueCredentialDto.goalCode, parentThreadId: issueCredentialDto.parentThreadId, willConfirm: issueCredentialDto.willConfirm, label: issueCredentialDto.label, autoAcceptCredential: issueCredentialDto.autoAcceptCredential };
let payload;
if (IssueCredentialType.INDY === issueCredentialDto.credentialType) {
payload = { attributes: issueCredentialDto.attributes, comment: issueCredentialDto.comment, credentialDefinitionId: issueCredentialDto.credentialDefinitionId, orgId: issueCredentialDto.orgId, protocolVersion: issueCredentialDto.protocolVersion, goalCode: issueCredentialDto.goalCode, parentThreadId: issueCredentialDto.parentThreadId, willConfirm: issueCredentialDto.willConfirm, label: issueCredentialDto.label, autoAcceptCredential: issueCredentialDto.autoAcceptCredential, credentialType: issueCredentialDto.credentialType };
}
if (IssueCredentialType.JSONLD === issueCredentialDto.credentialType) {
payload = { credential: issueCredentialDto.credential, options:issueCredentialDto.options, comment: issueCredentialDto.comment, orgId: issueCredentialDto.orgId, protocolVersion: issueCredentialDto.protocolVersion, goalCode: issueCredentialDto.goalCode, parentThreadId: issueCredentialDto.parentThreadId, willConfirm: issueCredentialDto.willConfirm, label: issueCredentialDto.label, autoAcceptCredential: issueCredentialDto.autoAcceptCredential, credentialType: issueCredentialDto.credentialType };
}

return this.sendNats(this.issuanceProxy, 'send-credential-create-offer-oob', payload);
}

Expand Down
Loading

0 comments on commit 7e36041

Please sign in to comment.