Skip to content
Snippets Groups Projects
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
service.ts 14.54 KiB
import type CredentialDto from '../entities/credential.entity.js';
import type CredentialTypeDto from '../entities/credentialType.entity.js';
import type OfferCredentialDto from '../entities/entity.js';
import type GetIssueCredentialsDto from '../entities/get-issue-credentials.dto.js';
import type ProposeCredentialDto from '../entities/propose-credential.dto.js';
import type { Credential, Prisma } from '@prisma/client';

import {
  BadRequestException,
  Injectable,
  PreconditionFailedException,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';

import NatsClientService from '../../client/nats.client.js';
import RestClientService from '../../client/rest.client.js';
import TSAClientService from '../../client/tsa.client.js';
import { TSAService } from '../../common/constants.js';
import CredentialDefService from '../../credentialDef/services/service.js';
import PrismaService from '../../prisma/prisma.service.js';
import Utils from '../../utils/common.js';
import logger from '../../utils/logger.js';
import pagination from '../../utils/pagination.js';
import CredentialRepository from '../repository/credential.repository.js';
import CredentialsTypeRepository from '../repository/credentialType.repository.js';

@Injectable()
export default class AttestationService {
  private credentialRepository: CredentialRepository;

  private credentialRepositoryType: CredentialsTypeRepository;

  public constructor(
    private readonly credDefService: CredentialDefService,
    private readonly prismaService: PrismaService,
    private readonly restClient: RestClientService,
    private readonly natsClient: NatsClientService,
    private readonly tsaClient: TSAClientService,
    private readonly configService: ConfigService,
  ) {
    this.credentialRepository = new CredentialRepository(this.prismaService);
    this.credentialRepositoryType = new CredentialsTypeRepository(
      this.prismaService,
    );
  }

  public static readonly status = {
    OFFER_SENT: 'offer-sent',
    PROPOSAL_SENT: 'proposal-sent',
    REQUEST_RECEIVED: 'request-received',
    DONE: 'done',
    OFFER_RECEIVED: 'offer-received',
  };

  public static readonly principalMemberCredential =
    'principalMemberCredential';

  public static readonly connectionStatus = {
    TRUSTED: 'trusted',
  };

  public async createOfferCredential(
    credentialRequest: OfferCredentialDto,
    isTrustedConnectionRequired = false,
  ) {
    // TODO is it a correct conditions here? Should not be just isTrustedConnectionRequired?
    if (!isTrustedConnectionRequired) {
      const connection = await this.getConnectionByID(
        credentialRequest.connectionId,
      );

      logger.info(`connection ${JSON.stringify(connection)}`);

      if (connection?.status !== AttestationService.connectionStatus.TRUSTED) {
        return null;
      }
    }

    const agentUrl = this.configService.get('agent.AGENT_URL');
    const credentialRequestObj = { ...credentialRequest };

    const credDef = await this.findCredDef(
      credentialRequestObj.credentialDefinitionId,
    );

    const expirationDate = Utils.calculateExpiry(credDef.expiryHours);

    if (expirationDate) {
      credentialRequestObj.attributes.push({
        name: 'expirationDate',
        value: expirationDate.toString(),
      });
    }

    const schemaDetails = await this.getSchemaAndAttributesBySchemaIDFromLedger(
      credDef.schemaID,
    );
    logger.info(
      `schemaDetails?.attrNames?.length ${schemaDetails?.attrNames?.length}`,
    );
    logger.info(
      `credentialRequest.preview.attributes.length ${credentialRequest.attributes.length}`,
    );

    if (
      schemaDetails?.attrNames?.length !== credentialRequest.attributes.length
    ) {
      throw new BadRequestException('Invalid attributes');
    }

    logger.info(`offer-credential payload: ${credentialRequestObj}`);

    try {
      const credentialRequestPayload = {
        connectionId: credentialRequestObj.connectionId,
        credentialDefinitionId: credentialRequestObj.credentialDefinitionId,
        comment: credentialRequestObj.comment,
        preview: {
          '@type':
            'https://didcomm.org/issue-credential/1.0/credential-preview',
          attributes: credentialRequestObj.attributes,
        },
        autoAcceptCredential: credentialRequestObj.autoAcceptCredential,
      };

      logger.info(
        `***Offer Credential Payload***    ${JSON.stringify(
          credentialRequestPayload,
        )}`,
      );

      const responseData = await this.restClient.post(
        `${agentUrl}/credentials/offer-credential`,
        credentialRequestPayload,
      );
      logger.info(responseData);
      return responseData;
    } catch (error) {
      logger.error(JSON.stringify(error));
      throw new Error(JSON.stringify(error));
    }
  }

  public async proposeCredential(connectionCreate: ProposeCredentialDto) {
    const agentUrl = this.configService.get('agent.AGENT_URL');
    const connectionCreateObj = { ...connectionCreate };
    try {
      const responseData = await this.restClient.post(
        `${agentUrl}/credentials/propose-credential`,
        connectionCreateObj,
      );
      logger.info(responseData);
      return responseData;
    } catch (error) {
      logger.error(JSON.stringify(error));
      throw new Error(JSON.stringify(error));
    }
  }

  public async acceptRequestCredential(credentialId: string) {
    const agentUrl = this.configService.get('agent.AGENT_URL');

    const responseData = await this.restClient.post(
      `${agentUrl}/credentials/${credentialId}/accept-request`,
      {},
    );
    logger.info(responseData);
    return responseData;
  }

  public async acceptProposeCredential(credentialId: string) {
    const agentUrl = this.configService.get('agent.AGENT_URL');
    const responseData = await this.restClient.post(
      `${agentUrl}/credentials/${credentialId}/accept-proposal`,
      {},
    );
    logger.info(responseData);
    return responseData;
  }

  public async acceptCredentialOffer(credentialId: string) {
    const agentUrl = this.configService.get('agent.AGENT_URL');
    const responseData = await this.restClient.post(
      `${agentUrl}/credentials/${credentialId}/accept-offer`,
      {},
    );
    logger.info(responseData);
    return responseData;
  }

  public async acceptCredential(credentialId: string) {
    const agentUrl = this.configService.get('agent.AGENT_URL');
    const responseData = await this.restClient.post(
      `${agentUrl}/credentials/${credentialId}/accept-credential`,
      {},
    );
    logger.info(responseData);
    return responseData;
  }

  public async createCredential(credential: CredentialDto) {
    const connection = await this.getConnectionByID(credential.connectionId);

    const credDef = await this.findCredDef(credential.credDefId);

    logger.info(`credDef.expiryHours ${credDef.expiryHours}`);
    const expirationDate = Utils.calculateExpiry(credDef.expiryHours);
    logger.info(`expirationDate ${expirationDate}`);
    const tempCredential = credential;
    delete tempCredential.schemaId;

    return this.credentialRepository.createCredential({
      ...tempCredential,
      ...(expirationDate !== 'NA' && { expirationDate }),
      principalDid: connection.theirDid,
    });
  }

  public async getConnectionByID(connectionID: string) {
    const connection = await this.natsClient.getConnectionById(connectionID);
    return connection;
  }

  public async updateCredential(credential: CredentialDto) {
    return this.credentialRepository.updateCredential({
      where: { credentialId: credential.credentialId },
      data: {
        state: credential.state,
        updatedDate: new Date(),
      },
    });
  }

  public findCredentialById(credentialId: string) {
    const where: Prisma.CredentialWhereUniqueInput = { credentialId };
    return this.credentialRepository.findUniqueCredential({ where });
  }

  public findCredentialByThreadId(threadId: string) {
    const where: Prisma.CredentialWhereUniqueInput = { threadId };
    return this.credentialRepository.findUniqueCredential({ where });
  }

  public async findCredential(
    pageSize: number,
    page: number,
    isReceived: boolean,
    state?: string | false,
    credDefId?: string | false,
    createdDateStart?: string | false,
    createdDateEnd?: string | false,
    updatedDateStart?: string | false,
    updatedDateEnd?: string | false,
    expirationDateStart?: string | false,
    expirationDateEnd?: string | false,
    connectionId?: string | false,
    principalDid?: string | false,
  ) {
    let query: {
      skip?: number;
      take?: number;
      cursor?: Prisma.CredentialWhereUniqueInput;
      where: Prisma.CredentialWhereInput;
      orderBy?: Prisma.CredentialOrderByWithRelationInput;
    } = {
      where: {},
    };

    if (state) {
      const states: string[] = state.split(',');
      query.where.state = { in: states };
    }
    if (credDefId) {
      query.where.credDefId = credDefId;
    }
    if (createdDateStart) {
      query.where.createdDate = { gte: createdDateStart };
    }
    if (createdDateEnd) {
      query.where.createdDate = Object.assign({}, query.where.createdDate, {
        lte: createdDateEnd,
      });
    }
    if (updatedDateStart) {
      query.where.updatedDate = { gte: updatedDateStart };
    }
    if (updatedDateEnd) {
      query.where.updatedDate = Object.assign({}, query.where.updatedDate, {
        lte: updatedDateEnd,
      });
    }
    if (expirationDateStart) {
      query.where.expirationDate = { gte: expirationDateStart };
    }
    if (expirationDateEnd) {
      query.where.expirationDate = Object.assign(
        {},
        query.where.expirationDate,
        { lte: expirationDateEnd },
      );
    }
    if (connectionId) {
      query.where.connectionId = connectionId;
    }
    if (principalDid) {
      query.where.principalDid = principalDid;
    }

    if (isReceived) {
      // TODO we need to check the case when first and second OCMs can re-use the same connection
      // and can issue credentials to each other. Will this function returns correct results for
      // every OCM?
      const receivedConnections =
        await this.natsClient.getReceivedConnections();
      if (
        Array.isArray(receivedConnections) &&
        receivedConnections.length > 0
      ) {
        const receivedConnectionIds = receivedConnections.map(
          (connection) => connection.connectionId,
        );

        query.where.connectionId = { in: receivedConnectionIds };
      }
    }

    query = { ...query, ...pagination(pageSize, page) };

    return this.credentialRepository.findCredential(query);
  }

  public async issueMemberCredentials(data: {
    status: string;
    connectionId: string;
    theirLabel: string;
    participantDID: string;
    theirDid: string;
    credDefId: string;
    attributes: { name: string; value: string }[];
    autoAcceptCredential: string;
  }) {
    logger.info(JSON.stringify(data));
    const payload: OfferCredentialDto = {
      connectionId: data.connectionId,
      credentialDefinitionId: data.credDefId,
      comment: 'Created',
      attributes: data.attributes,
      autoAcceptCredential: data.autoAcceptCredential,
    };
    logger.info(JSON.stringify(payload));

    const tsaResponse = await this.tsaClient.getPolicy(
      `${this.configService.get('TSA_URL')}/${
        TSAService.PRINCIPAL_CREDENTIAL_REQUEST
      }/1.0/evaluation`,
    );

    if (tsaResponse?.success && !tsaResponse.returnData) {
      throw new PreconditionFailedException('TSA ERROR!');
    }

    const result = await this.createOfferCredential(payload, true);
    logger.info(JSON.stringify(result));
    return result;
  }

  public getPrincipalMemberShipCredentials(data: { type: string }) {
    return this.credentialRepositoryType.findUniqueCredentialsType(data);
  }

  public async getSchemaAndAttributesBySchemaIDFromLedger(schemaID: string) {
    const agentUrl = this.configService.get('agent.AGENT_URL');
    const responseData = await this.restClient.get(
      `${agentUrl}/schemas/${schemaID}`,
    );

    if (!responseData?.id) {
      throw new BadRequestException('Invalid schema ID');
    }

    return responseData;
  }

  public updateSchemaByType(type: string, body: { schemaId: string }) {
    return this.credentialRepositoryType.updateCredentialsType({
      where: {
        type,
      },
      data: {
        schemaId: body.schemaId,
      },
    });
  }

  public async getIssueCredentials(data: GetIssueCredentialsDto) {
    return this.credentialRepository.findCredential({
      where: {
        connectionId: data.connectionId,
      },
    });
  }

  public async getCredentialInformation(credentialId: string) {
    const agentUrl = this.configService.get('agent.AGENT_URL');
    const responseData = await this.restClient.get(
      `${agentUrl}/credentials/${credentialId}`,
    );

    if (!responseData?.id) {
      throw new BadRequestException('Invalid credential ID');
    }

    return responseData;
  }

  public async deleteCredential(credentialId: string) {
    const agentUrl = this.configService.get('agent.AGENT_URL');
    const responseData = await this.restClient.delete(
      `${agentUrl}/credentials/${credentialId}`,
    );

    await this.credentialRepository.deleteCredential({
      where: {
        credentialId,
      },
    });

    return responseData;
  }

  public createCredentialsType(credentialType: CredentialTypeDto) {
    return this.credentialRepositoryType.createCredentialsType({
      type: credentialType.type,
      schemaId: credentialType.schemaId,
    });
  }

  public connectionTrusted(connectionId: string) {
    return this.natsClient.connectionTrusted(connectionId);
  }

  public async findCredDef(credentialDefinitionId: string) {
    const credDefRes = await this.credDefService.findCredentialDefById(
      credentialDefinitionId,
    );

    if (!credDefRes[0]) {
      return {
        expiryHours: '-1',
        schemaID: '',
      };
    }

    return credDefRes[1][0];
  }

  public async findReceivedCredentials() {
    try {
      let result: Credential[] = [];
      const receivedConnections =
        await this.natsClient.getReceivedConnections();

      if (
        Array.isArray(receivedConnections) &&
        receivedConnections.length > 0
      ) {
        const receivedConnectionIds = receivedConnections.map(
          (connection) => connection.connectionId,
        );

        const credentials = await this.credentialRepository.findCredential({
          where: { connectionId: { in: receivedConnectionIds } },
        });
        [, result] = credentials;
      }

      return result;
    } catch (error) {
      logger.error(JSON.stringify(error));
      throw new Error(JSON.stringify(error));
    }
  }
}