diff --git a/.env.sample b/.env.sample index 17873880c..e36d252e7 100644 --- a/.env.sample +++ b/.env.sample @@ -1,7 +1,6 @@ MODE=DEV - -SUPABASE_URL= // Please specify your Supabase Url +SUPABASE_URL= // Please specify your Supabase URL SUPABASE_KEY= // Please specify your Supabase Anon key SUPABASE_JWT_SECRET= // Please specify your Supabase jwt secret @@ -9,7 +8,19 @@ API_GATEWAY_PROTOCOL=http API_GATEWAY_HOST='0.0.0.0' API_GATEWAY_PORT=5000 -PLATFORM_NAME=CREDEBL +## +PLATFORM_NAME= // Please specify your paltform name +PUBLIC_PLATFORM_SUPPORT_EMAIL= // Please specify your support email +POWERED_BY= // Please specify your powered by org name +PLATFORM_WEB_URL= // Please specify your platform web URL +POWERED_BY_URL= // Please specify your support URL + +PUBLIC_LOCALHOST_URL= // Please specify your localhost URL +PUBLIC_DEV_API_URL= // Please specify your DEV environment api URL +PUBLIC_QA_API_URL= // Please specify your your QA environment api URL +PUBLIC_PRODUCTION_API_URL= // Please specify your PRODUCTION environment api URL +PUBLIC_SANDBOX_API_URL= // Please specify your sandbox environment URL + AGENT_HOST=username@0.0.0.0 // Please specify your agent host VM and IP address AWS_ACCOUNT_ID=xxxxx // Please provide your AWS account Id @@ -47,8 +58,8 @@ PLATFORM_SEED= // The seed should consist of 32 characters. PLATFORM_ID= AFJ_AGENT_ENDPOINT_PATH=/apps/agent-provisioning/AFJ/endpoints/ -DATABASE_URL="postgresql://postgres:xxxxxx@localhost:5432/postgres?schema=public" #Provide supabase postgres url and Use the correct user/pwd, IP Address -POOL_DATABASE_URL="" #Provide pooler supabase postgres url +DATABASE_URL="postgresql://postgres:xxxxxx@localhost:5432/postgres?schema=public" #Provide supabase postgres URL and Use the correct user/pwd, IP Address +POOL_DATABASE_URL="" #Provide pooler supabase postgres URL CLUSTER_NAME="" # ecs cluster TESKDEFINITION_FAMILY="" # ecs task-definition AGENT_PROTOCOL=http @@ -68,4 +79,11 @@ export DEBUG="prisma:engine" export DEBUG="prisma:client" # enable both prisma-client- and engine-level debugging output -export DEBUG="prisma:client,prisma:engine" \ No newline at end of file +export DEBUG="prisma:client,prisma:engine" + +KEYCLOAK_DOMAIN=http://localhost:8080/ +KEYCLOAK_ADMIN_URL=http://localhost:8080 +KEYCLOAK_MASTER_REALM=xxxxxxx +KEYCLOAK_MANAGEMENT_CLIENT_ID=xxxxxxx +KEYCLOAK_MANAGEMENT_CLIENT_SECRET=xxxxxxx +KEYCLOAK_REALM=xxxxxxx \ No newline at end of file diff --git a/Dockerfiles/Dockerfile.agent-provisioning b/Dockerfiles/Dockerfile.agent-provisioning index 96311afdc..8d1eb9d21 100644 --- a/Dockerfiles/Dockerfile.agent-provisioning +++ b/Dockerfiles/Dockerfile.agent-provisioning @@ -8,10 +8,23 @@ USER nonroot # RUN apk add aws-cli -RUN npm install --ignore-scripts -g pnpm \ - && apk update \ - && apk add openssh-client \ - && apk add aws-cli +RUN set -eux \ + && apk --no-cache add \ + openssh-client \ + aws-cli \ + docker \ + docker-compose \ + && npm install -g pnpm --ignore-scripts \ + && export PATH=$PATH:/usr/lib/node_modules/pnpm/bin \ + && rm -rf /var/cache/apk/* + +RUN docker --version && \ + docker-compose --version + +ARG ROOT_PATH +ENV ROOT_PATH ${ROOT_PATH} + +RUN echo "ROOT_PATH is set to: $ROOT_PATH" # Set the working directory WORKDIR /app @@ -39,10 +52,15 @@ RUN addgroup -S nonroot \ USER nonroot # RUN apk add aws-cli -RUN npm install --ignore-scripts -g pnpm \ - && apk update \ - && apk add openssh-client \ - && apk add aws-cli +RUN set -eux \ + && apk --no-cache add \ + openssh-client \ + aws-cli \ + docker \ + docker-compose \ + && npm install -g pnpm --ignore-scripts \ + && export PATH=$PATH:/usr/lib/node_modules/pnpm/bin \ + && rm -rf /var/cache/apk/* WORKDIR /app @@ -51,6 +69,7 @@ RUN mkdir -p ./agent-provisioning/AFJ/agent-config RUN mkdir -p ./agent-provisioning/AFJ/port-file RUN mkdir -p ./agent-provisioning/AFJ/token + # Copy the compiled code COPY --from=build /app/dist/apps/agent-provisioning/ ./dist/apps/agent-provisioning/ COPY --from=build /app/node_modules ./node_modules @@ -60,15 +79,16 @@ COPY --from=build /app/apps/agent-provisioning/AFJ/port-file ./agent-provisionin # Set permissions RUN chmod +x /app/agent-provisioning/AFJ/scripts/start_agent.sh RUN chmod +x /app/agent-provisioning/AFJ/scripts/start_agent_ecs.sh +RUN chmod +x /app/agent-provisioning/AFJ/scripts/docker_start_agent.sh RUN chmod 777 /app/agent-provisioning/AFJ/endpoints RUN chmod 777 /app/agent-provisioning/AFJ/agent-config RUN chmod 777 /app/agent-provisioning/AFJ/token + # Copy the libs folder COPY libs/ ./libs/ # Set the command to run the microservice -CMD ["sh", "-c", "cd libs/prisma-service && npx prisma migrate deploy && npx prisma generate && cd ../.. && node dist/apps/agent-provisioning/main.js"] - +CMD ["sh", "-c", "cd libs/prisma-service && npx prisma migrate deploy && npx prisma generate && cd ../.. && node dist/apps/agent-provisioning/main.js $ROOT_PATH"] # docker build -t agent-provisioning-service -f Dockerfiles/Dockerfile.agent-provisioning . # docker run -d --env-file .env --name agent-provisioning-service docker.io/library/agent-provisioning-service \ No newline at end of file diff --git a/Dockerfiles/Dockerfile.agent-service b/Dockerfiles/Dockerfile.agent-service index bdc70ac46..288112c33 100644 --- a/Dockerfiles/Dockerfile.agent-service +++ b/Dockerfiles/Dockerfile.agent-service @@ -1,11 +1,14 @@ # Stage 1: Build the application FROM node:18-alpine as build -RUN addgroup -S nonroot \ - && adduser -S nonroot -G nonroot - -USER nonroot -RUN npm install --ignore-scripts -g pnpm +RUN npm install -g pnpm --ignore-scripts \ + && apk update \ + && apk add openssh-client \ + && apk add aws-cli \ + && apk add docker \ + && apk add docker-compose + +RUN npm install -g pnpm # Set the working directory WORKDIR /app @@ -25,11 +28,15 @@ RUN pnpm run build agent-service # Stage 2: Create the final image FROM node:18-alpine -RUN addgroup -S nonroot \ - && adduser -S nonroot -G nonroot +RUN npm install -g pnpm --ignore-scripts \ + && apk update \ + && apk add openssh-client \ + && apk add aws-cli \ + && apk add docker \ + && apk add docker-compose -USER nonroot -RUN npm install --ignore-scripts -g pnpm +RUN npm install -g pnpm +# Set the working directory WORKDIR /app # Copy the compiled code from the build stage diff --git a/Dockerfiles/Dockerfile.notification b/Dockerfiles/Dockerfile.notification new file mode 100644 index 000000000..eb7c78963 --- /dev/null +++ b/Dockerfiles/Dockerfile.notification @@ -0,0 +1,41 @@ +# Stage 1: Build the application +FROM node:18-alpine as build +RUN npm install -g pnpm --ignore-scripts +# Set the working directory +WORKDIR /app + +# Copy package.json and package-lock.json +COPY package.json ./ +#COPY package-lock.json ./ + +# Install dependencies +RUN pnpm i --ignore-scripts + +# Copy the rest of the application code +COPY . . +RUN cd libs/prisma-service && npx prisma migrate deploy && npx prisma generate + +# Build the notification service +RUN npm run build notification + + +# Stage 2: Create the final image +FROM node:18-alpine +RUN npm install -g pnpm --ignore-scripts +# Set the working directory +WORKDIR /app + +# Copy the compiled code from the build stage +COPY --from=build /app/dist/apps/notification/ ./dist/apps/notification/ + +# Copy the libs folder from the build stage +COPY --from=build /app/libs/ ./libs/ +#COPY --from=build /app/package.json ./ +COPY --from=build /app/node_modules ./node_modules + +# Set the command to run the microservice +CMD ["sh", "-c", "cd libs/prisma-service && npx prisma migrate deploy && npx prisma generate && cd ../.. && node dist/apps/notification/main.js"] + +# docker build -t notification -f Dockerfiles/Dockerfile.notification . +# docker run -d --env-file .env --name notification docker.io/library/notification +# docker logs -f notification diff --git a/Dockerfiles/Dockerfile.user b/Dockerfiles/Dockerfile.user index b1a096efa..07ac91bb0 100644 --- a/Dockerfiles/Dockerfile.user +++ b/Dockerfiles/Dockerfile.user @@ -8,6 +8,7 @@ RUN npm install --ignore-scripts -g pnpm # We don't need the standalone Chromium ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD true +ENV PUPPETEER_SKIP_DOWNLOAD true # Install Google Chrome Stable and fonts # Note: this installs the necessary libs to make the browser work with Puppeteer. @@ -46,6 +47,7 @@ USER nonroot RUN npm install --ignore-scripts -g pnpm # We don't need the standalone Chromium ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD true +ENV PUPPETEER_SKIP_DOWNLOAD true # Install Google Chrome Stable and fonts # Note: this installs the necessary libs to make the browser work with Puppeteer. @@ -74,4 +76,4 @@ CMD ["sh", "-c", "cd libs/prisma-service && npx prisma migrate deploy && npx pri # docker build -t user -f Dockerfiles/Dockerfile.user . # docker run -d --env-file .env --name user docker.io/library/user -# docker logs -f user \ No newline at end of file +# docker logs -f user diff --git a/apps/agent-provisioning/AFJ/scripts/docker_start_agent.sh b/apps/agent-provisioning/AFJ/scripts/docker_start_agent.sh new file mode 100644 index 000000000..ab3673453 --- /dev/null +++ b/apps/agent-provisioning/AFJ/scripts/docker_start_agent.sh @@ -0,0 +1,259 @@ +#!/bin/sh + +START_TIME=$(date +%s) + +AGENCY=$1 +EXTERNAL_IP=$2 +WALLET_NAME=$3 +WALLET_PASSWORD=$4 +RANDOM_SEED=$5 +WEBHOOK_HOST=$6 +WALLET_STORAGE_HOST=$7 +WALLET_STORAGE_PORT=$8 +WALLET_STORAGE_USER=$9 +WALLET_STORAGE_PASSWORD=${10} +CONTAINER_NAME=${11} +PROTOCOL=${12} +TENANT=${13} +AFJ_VERSION=${14} +INDY_LEDGER=${15} + +echo "AGENCY: $AGENCY" +echo "EXTERNAL_IP: $EXTERNAL_IP" +echo "WALLET_NAME: $WALLET_NAME" +echo "WALLET_PASSWORD: $WALLET_PASSWORD" +echo "RANDOM_SEED: $RANDOM_SEED" +echo "WEBHOOK_HOST: $WEBHOOK_HOST" +echo "WALLET_STORAGE_HOST: $WALLET_STORAGE_HOST" +echo "WALLET_STORAGE_PORT: $WALLET_STORAGE_PORT" +echo "WALLET_STORAGE_USER: $WALLET_STORAGE_USER" +echo "WALLET_STORAGE_PASSWORD: $WALLET_STORAGE_PASSWORD" +echo "CONTAINER_NAME: $CONTAINER_NAME" +echo "PROTOCOL: $PROTOCOL" +echo "TENANT: $TENANT" +echo "AFJ_VERSION: $AFJ_VERSION" +echo "INDY_LEDGER: $INDY_LEDGER" + +ADMIN_PORT_FILE="$PWD/agent-provisioning/AFJ/port-file/last-admin-port.txt" +INBOUND_PORT_FILE="$PWD/agent-provisioning/AFJ/port-file/last-inbound-port.txt" +ADMIN_PORT=8001 +INBOUND_PORT=9001 + +increment_port() { + local port="$1" + local lower_limit="$2" + + while [ "$port" -le "$lower_limit" ]; do + port=$((port + 1)) # Increment the port using arithmetic expansion + done + + echo "$port" +} + +# Check if admin port file exists and if not, create and initialize it +if [ ! -e "$ADMIN_PORT_FILE" ]; then + echo "$ADMIN_PORT" > "$ADMIN_PORT_FILE" +fi + +# Read the last used admin port number from the file +last_used_admin_port=$(cat "$ADMIN_PORT_FILE") +echo "Last used admin port: $last_used_admin_port" + +# Increment the admin port number starting from the last used port +last_used_admin_port=$(increment_port "$last_used_admin_port" "$last_used_admin_port") + +# Save the updated admin port number back to the file and update the global variable +echo "$last_used_admin_port" > "$ADMIN_PORT_FILE" +ADMIN_PORT="$last_used_admin_port" + +# Check if inbound port file exists and if not, create and initialize it +if [ ! -e "$INBOUND_PORT_FILE" ]; then + echo "$INBOUND_PORT" > "$INBOUND_PORT_FILE" +fi + +# Read the last used inbound port number from the file +last_used_inbound_port=$(cat "$INBOUND_PORT_FILE") +echo "Last used inbound port: $last_used_inbound_port" + +# Increment the inbound port number starting from the last used port +last_used_inbound_port=$(increment_port "$last_used_inbound_port" "$last_used_inbound_port") + +# Save the updated inbound port number back to the file and update the global variable +echo "$last_used_inbound_port" > "$INBOUND_PORT_FILE" +INBOUND_PORT="$last_used_inbound_port" + +echo "Last used admin port: $ADMIN_PORT" +echo "Last used inbound port: $INBOUND_PORT" + +echo "AGENT SPIN-UP STARTED" + +if [ -d "${PWD}/agent-provisioning/AFJ/endpoints" ]; then + echo "Endpoints directory exists." +else + echo "Error: Endpoints directory does not exists." + mkdir ${PWD}/agent-provisioning/AFJ/endpoints +fi + +if [ -d "${PWD}/agent-provisioning/AFJ/agent-config" ]; then + echo "Endpoints directory exists." +else + echo "Error: Endpoints directory does not exists." + mkdir ${PWD}/agent-provisioning/AFJ/agent-config +fi + +AGENT_ENDPOINT="${PROTOCOL}://${EXTERNAL_IP}:${INBOUND_PORT}" + +echo "-----$AGENT_ENDPOINT----" +CONFIG_FILE="${PWD}/agent-provisioning/AFJ/agent-config/${AGENCY}_${CONTAINER_NAME}.json" + +# Check if the file exists +if [ -f "$CONFIG_FILE" ]; then + # If it exists, remove the file + rm "$CONFIG_FILE" +fi + + +cat <${CONFIG_FILE} +{ + "label": "${AGENCY}_${CONTAINER_NAME}", + "walletId": "$WALLET_NAME", + "walletKey": "$WALLET_PASSWORD", + "walletType": "postgres", + "walletUrl": "$WALLET_STORAGE_HOST:$WALLET_STORAGE_PORT", + "walletAccount": "$WALLET_STORAGE_USER", + "walletPassword": "$WALLET_STORAGE_PASSWORD", + "walletAdminAccount": "$WALLET_STORAGE_USER", + "walletAdminPassword": "$WALLET_STORAGE_PASSWORD", + "walletScheme": "DatabasePerWallet", + "indyLedger": $INDY_LEDGER, + "endpoint": [ + "$AGENT_ENDPOINT" + ], + "autoAcceptConnections": true, + "autoAcceptCredentials": "contentApproved", + "autoAcceptProofs": "contentApproved", + "logLevel": 5, + "inboundTransport": [ + { + "transport": "$PROTOCOL", + "port": "$INBOUND_PORT" + } + ], + "outboundTransport": [ + "$PROTOCOL" + ], + "webhookUrl": "$WEBHOOK_HOST/wh/$AGENCY", + "adminPort": "$ADMIN_PORT", + "tenancy": $TENANT +} +EOF + +FILE_NAME="docker-compose_${AGENCY}_${CONTAINER_NAME}.yaml" + +DOCKER_COMPOSE="${PWD}/agent-provisioning/AFJ/${FILE_NAME}" + +# Check if the file exists +if [ -f "$DOCKER_COMPOSE" ]; then + # If it exists, remove the file + rm "$DOCKER_COMPOSE" +fi +echo ${PWD} +cat <${DOCKER_COMPOSE} +version: '3' + +services: + agent: + image: $AFJ_VERSION + + container_name: ${AGENCY}_${CONTAINER_NAME} + restart: always + environment: + AFJ_REST_LOG_LEVEL: 1 + ROOT_PATH: ${ROOT_PATH} + ports: + - ${INBOUND_PORT}:${INBOUND_PORT} + - ${ADMIN_PORT}:${ADMIN_PORT} + + volumes: + - ${ROOT_PATH}:/agent-config + + command: --auto-accept-connections --config /agent-config/${AGENCY}_${CONTAINER_NAME}.json + +volumes: + pgdata: + agent-indy_client: + agent-tmp: +EOF + +if [ $? -eq 0 ]; then + cd agent-provisioning/AFJ + echo "docker-compose generated successfully!" + echo "=================" + echo "spinning up the container" + echo "=================" + echo "container-name::::::${CONTAINER_NAME}" + echo "file-name::::::$FILE_NAME" + + docker compose -f $FILE_NAME up -d + if [ $? -eq 0 ]; then + + n=0 + until [ "$n" -ge 6 ]; do + if netstat -tln | grep ${ADMIN_PORT} >/dev/null; then + + AGENTURL="http://${EXTERNAL_IP}:${ADMIN_PORT}/agent" + agentResponse=$(curl -s -o /dev/null -w "%{http_code}" $AGENTURL) + + if [ "$agentResponse" = "200" ]; then + echo "Agent is running" && break + else + echo "Agent is not running" + n=$((n + 1)) + sleep 10 + fi + else + echo "No response from agent" + n=$((n + 1)) + sleep 10 + fi + done + + echo "Creating agent config" + # Capture the logs from the container + container_logs=$(docker logs $(docker ps -q --filter "name=${AGENCY}_${CONTAINER_NAME}")) + + # Extract the token from the logs using sed + token=$(echo "$container_logs" | sed -nE 's/.*API Toekn: ([^ ]+).*/\1/p') + + # Print the extracted token + echo "Token: $token" + + ENDPOINT="${PWD}/endpoints/${AGENCY}_${CONTAINER_NAME}.json" + + # Check if the file exists + if [ -f "$ENDPOINT" ]; then + # If it exists, remove the file + rm "$ENDPOINT" + fi + cat <${ENDPOINT} + { + "CONTROLLER_ENDPOINT":"${EXTERNAL_IP}:${ADMIN_PORT}" + } +EOF + + cat <${PWD}/token/${AGENCY}_${CONTAINER_NAME}.json + { + "token" : "$token" + } +EOF + echo "Agent config created" + else + echo "===============" + echo "ERROR : Failed to spin up the agent!" + echo "===============" && exit 125 + fi +else + echo "ERROR : Failed to execute!" && exit 125 +fi + +echo "Total time elapsed: $(date -ud "@$(($(date +%s) - $START_TIME))" +%T) (HH:MM:SS)" \ No newline at end of file diff --git a/apps/agent-provisioning/AFJ/scripts/start_agent.sh b/apps/agent-provisioning/AFJ/scripts/start_agent.sh old mode 100644 new mode 100755 index 37047815c..87a595939 --- a/apps/agent-provisioning/AFJ/scripts/start_agent.sh +++ b/apps/agent-provisioning/AFJ/scripts/start_agent.sh @@ -227,7 +227,7 @@ if [ $? -eq 0 ]; then container_logs=$(docker logs $(docker ps -q --filter "name=${AGENCY}_${CONTAINER_NAME}")) # Extract the token from the logs using sed - token=$(echo "$container_logs" | sed -nE 's/.*API Toekn: ([^ ]+).*/\1/p') + token=$(echo "$container_logs" | sed -nE 's/.*API Token: ([^ ]+).*/\1/p') # Print the extracted token echo "Token: $token" diff --git a/apps/agent-service/src/agent-service.controller.ts b/apps/agent-service/src/agent-service.controller.ts index 8e96d70b9..386e6b1a2 100644 --- a/apps/agent-service/src/agent-service.controller.ts +++ b/apps/agent-service/src/agent-service.controller.ts @@ -1,11 +1,9 @@ import { Controller } from '@nestjs/common'; import { MessagePattern } from '@nestjs/microservices'; import { AgentServiceService } from './agent-service.service'; -import { IAgentStatus, IConnectionDetails, IUserRequestInterface, ISendProofRequestPayload, IAgentSpinUpSatus, IGetCredDefAgentRedirection, IGetSchemaAgentRedirection, IAgentSpinupDto, IIssuanceCreateOffer, ITenantCredDef, ITenantDto, ITenantSchema, IOutOfBandCredentialOffer, IProofPresentation, IAgentProofRequest, IPresentation } from './interface/agent-service.interface'; +import { IAgentStatus, IConnectionDetails, IUserRequestInterface, ISendProofRequestPayload, IAgentSpinUpSatus, IGetCredDefAgentRedirection, IGetSchemaAgentRedirection, IAgentSpinupDto, IIssuanceCreateOffer, ITenantCredDef, ITenantDto, ITenantSchema, IOutOfBandCredentialOffer, IAgentProofRequest, IDidCreate, IWallet, ITenantRecord, ICreateConnectionInvitation } from './interface/agent-service.interface'; import { user } from '@prisma/client'; -import { ICreateConnectionUrl } from '@credebl/common/interfaces/connection.interface'; -import { IConnectionDetailsById } from 'apps/api-gateway/src/interfaces/IConnectionSearch.interface'; -import { IProofPresentationDetails } from '@credebl/common/interfaces/verification.interface'; +import { InvitationMessage } from '@credebl/common/interfaces/agent-service.interface'; @Controller() export class AgentServiceController { @@ -28,12 +26,31 @@ export class AgentServiceController { return this.agentServiceService.createTenant(payload.createTenantDto, payload.user); } + /** + * @returns did + */ + @MessagePattern({ cmd: 'create-did' }) + async createDid(payload: { createDidDto: IDidCreate, orgId: string, user: IUserRequestInterface }): Promise { + return this.agentServiceService.createDid(payload.createDidDto, payload.orgId, payload.user); + } + + @MessagePattern({ cmd: 'create-wallet' }) + async createWallet(payload: { createWalletDto: IWallet, user: IUserRequestInterface}): Promise { + return this.agentServiceService.createWallet(payload.createWalletDto); + } + //DONE @MessagePattern({ cmd: 'agent-create-schema' }) async createSchema(payload: ITenantSchema): Promise { return this.agentServiceService.createSchema(payload); } + //DONE + @MessagePattern({ cmd: 'agent-create-w3c-schema' }) + async createW3CSchema(payload: { url, orgId, schemaRequestPayload }): Promise { + return this.agentServiceService.createW3CSchema(payload.url, payload.orgId, payload.schemaRequestPayload); + } + //DONE @MessagePattern({ cmd: 'agent-get-schema' }) async getSchemaById(payload: IGetSchemaAgentRedirection): Promise { @@ -54,17 +71,17 @@ export class AgentServiceController { //DONE @MessagePattern({ cmd: 'agent-create-connection-legacy-invitation' }) - async createLegacyConnectionInvitation(payload: { connectionPayload: IConnectionDetails, url: string, apiKey: string }): Promise { - return this.agentServiceService.createLegacyConnectionInvitation(payload.connectionPayload, payload.url, payload.apiKey); + async createLegacyConnectionInvitation(payload: { connectionPayload: IConnectionDetails, url: string, orgId: string }): Promise { + return this.agentServiceService.createLegacyConnectionInvitation(payload.connectionPayload, payload.url, payload.orgId); } @MessagePattern({ cmd: 'agent-send-credential-create-offer' }) async sendCredentialCreateOffer(payload: { issueData: IIssuanceCreateOffer; url: string; - apiKey: string; + orgId: string; }): Promise { - return this.agentServiceService.sendCredentialCreateOffer(payload.issueData, payload.url, payload.apiKey); + return this.agentServiceService.sendCredentialCreateOffer(payload.issueData, payload.url, payload.orgId); } //DONE @@ -75,8 +92,8 @@ export class AgentServiceController { //DONE @MessagePattern({ cmd: 'agent-get-issued-credentials-by-credentialDefinitionId' }) - async getIssueCredentialsbyCredentialRecordId(payload: { url: string; apiKey: string }): Promise { - return this.agentServiceService.getIssueCredentialsbyCredentialRecordId(payload.url, payload.apiKey); + async getIssueCredentialsbyCredentialRecordId(payload: { url: string; orgId: string }): Promise { + return this.agentServiceService.getIssueCredentialsbyCredentialRecordId(payload.url, payload.orgId); } //DONE @MessagePattern({ cmd: 'agent-get-proof-presentations' }) @@ -86,8 +103,8 @@ export class AgentServiceController { //DONE @MessagePattern({ cmd: 'agent-get-proof-presentation-by-id' }) - async getProofPresentationById(payload: { url: string; apiKey: string }): Promise { - return this.agentServiceService.getProofPresentationById(payload.url, payload.apiKey); + async getProofPresentationById(payload: { url: string; orgId: string }): Promise { + return this.agentServiceService.getProofPresentationById(payload.url, payload.orgId); } //DONE @@ -95,25 +112,25 @@ export class AgentServiceController { async sendProofRequest(payload: { proofRequestPayload: ISendProofRequestPayload; url: string; - apiKey: string; + orgId: string; }): Promise { - return this.agentServiceService.sendProofRequest(payload.proofRequestPayload, payload.url, payload.apiKey); + return this.agentServiceService.sendProofRequest(payload.proofRequestPayload, payload.url, payload.orgId); } //DONE @MessagePattern({ cmd: 'agent-verify-presentation' }) - async verifyPresentation(payload: { url: string; apiKey: string }): Promise { - return this.agentServiceService.verifyPresentation(payload.url, payload.apiKey); + async verifyPresentation(payload: { url: string; orgId: string }): Promise { + return this.agentServiceService.verifyPresentation(payload.url, payload.orgId); } //DONE @MessagePattern({ cmd: 'agent-get-all-connections' }) - async getConnections(payload: { url: string; apiKey: string }): Promise { - return this.agentServiceService.getConnections(payload.url, payload.apiKey); + async getConnections(payload: { url: string; orgId: string }): Promise { + return this.agentServiceService.getConnections(payload.url, payload.orgId); } @MessagePattern({ cmd: 'agent-get-connection-details-by-connectionId' }) - async getConnectionsByconnectionId(payload: { url: string, apiKey: string }): Promise { - return this.agentServiceService.getConnectionsByconnectionId(payload.url, payload.apiKey); + async getConnectionsByconnectionId(payload: { url: string, orgId: string }): Promise { + return this.agentServiceService.getConnectionsByconnectionId(payload.url, payload.orgId); } /** @@ -126,59 +143,64 @@ export class AgentServiceController { return this.agentServiceService.getAgentHealthDetails(payload.orgId); } + @MessagePattern({ cmd: 'get-ledger-config' }) + async getLedgerConfig(payload: { user: IUserRequestInterface }): Promise { + return this.agentServiceService.getLedgerConfigDetails(payload.user); + } + //DONE @MessagePattern({ cmd: 'agent-send-out-of-band-proof-request' }) async sendOutOfBandProofRequest(payload: { proofRequestPayload: ISendProofRequestPayload; url: string; - apiKey: string; + orgId: string; }): Promise { - return this.agentServiceService.sendOutOfBandProofRequest(payload.proofRequestPayload, payload.url, payload.apiKey); + return this.agentServiceService.sendOutOfBandProofRequest(payload.proofRequestPayload, payload.url, payload.orgId); } //DONE @MessagePattern({ cmd: 'get-agent-verified-proof-details' }) - async getVerifiedProofDetails(payload: { url: string; apiKey: string }): Promise { - return this.agentServiceService.getVerifiedProofDetails(payload.url, payload.apiKey); + async getVerifiedProofDetails(payload: { url: string; orgId: string }): Promise { + return this.agentServiceService.getVerifiedProofDetails(payload.url, payload.orgId); } @MessagePattern({ cmd: 'agent-schema-endorsement-request' }) async schemaEndorsementRequest(payload: { url: string; - apiKey: string; + orgId: string; requestSchemaPayload: object; }): Promise { - return this.agentServiceService.schemaEndorsementRequest(payload.url, payload.apiKey, payload.requestSchemaPayload); + return this.agentServiceService.schemaEndorsementRequest(payload.url, payload.orgId, payload.requestSchemaPayload); } @MessagePattern({ cmd: 'agent-credDef-endorsement-request' }) async credDefEndorsementRequest(payload: { url: string; - apiKey: string; + orgId: string; requestSchemaPayload: object; }): Promise { return this.agentServiceService.credDefEndorsementRequest( payload.url, - payload.apiKey, + payload.orgId, payload.requestSchemaPayload ); } //DONE @MessagePattern({ cmd: 'agent-sign-transaction' }) - async signTransaction(payload: { url: string; apiKey: string; signEndorsementPayload: object }): Promise { - return this.agentServiceService.signTransaction(payload.url, payload.apiKey, payload.signEndorsementPayload); + async signTransaction(payload: { url: string; orgId: string; signEndorsementPayload: object }): Promise { + return this.agentServiceService.signTransaction(payload.url, payload.orgId, payload.signEndorsementPayload); } //DONE @MessagePattern({ cmd: 'agent-submit-transaction' }) - async submitTransaction(payload: { url: string; apiKey: string; submitEndorsementPayload: object }): Promise { - return this.agentServiceService.sumbitTransaction(payload.url, payload.apiKey, payload.submitEndorsementPayload); + async submitTransaction(payload: { url: string; orgId: string; submitEndorsementPayload: object }): Promise { + return this.agentServiceService.sumbitTransaction(payload.url, payload.orgId, payload.submitEndorsementPayload); } //DONE @MessagePattern({ cmd: 'agent-out-of-band-credential-offer' }) - async outOfBandCredentialOffer(payload: { outOfBandIssuancePayload: IOutOfBandCredentialOffer, url: string, apiKey: string }): Promise { - return this.agentServiceService.outOfBandCredentialOffer(payload.outOfBandIssuancePayload, payload.url, payload.apiKey); + async outOfBandCredentialOffer(payload: { outOfBandIssuancePayload: IOutOfBandCredentialOffer, url: string, orgId: string }): Promise { + return this.agentServiceService.outOfBandCredentialOffer(payload.outOfBandIssuancePayload, payload.url, payload.orgId); } @MessagePattern({ cmd: 'delete-wallet' }) @@ -186,26 +208,46 @@ export class AgentServiceController { return this.agentServiceService.deleteWallet(payload.url, payload.apiKey); } - @MessagePattern({ cmd: 'get-org-agent-api-key' }) - async getOrgAgentApiKey(payload: { orgId: string }): Promise { - return this.agentServiceService.getOrgAgentApiKey(payload.orgId); - } - @MessagePattern({ cmd: 'agent-receive-invitation-url' }) async receiveInvitationUrl(payload: { url, - apiKey, + orgId, receiveInvitationUrl }): Promise { - return this.agentServiceService.receiveInvitationUrl(payload.receiveInvitationUrl, payload.url, payload.apiKey); + return this.agentServiceService.receiveInvitationUrl(payload.receiveInvitationUrl, payload.url, payload.orgId); } @MessagePattern({ cmd: 'agent-receive-invitation' }) async receiveInvitation(payload: { url, - apiKey, + orgId, receiveInvitation }): Promise { - return this.agentServiceService.receiveInvitation(payload.receiveInvitation, payload.url, payload.apiKey); + return this.agentServiceService.receiveInvitation(payload.receiveInvitation, payload.url, payload.orgId); + } + + @MessagePattern({ cmd: 'agent-send-question' }) + async sendQuestion(payload: { + url, + orgId, + questionPayload + }): Promise { + + return this.agentServiceService.sendQuestion(payload.questionPayload, payload.url, payload.orgId); } + + @MessagePattern({ cmd: 'agent-get-question-answer-record' }) + async getQuestionAnswersRecord(payload: { url: string, orgId: string }): Promise { + return this.agentServiceService.getQuestionAnswersRecord(payload.url, payload.orgId); + } + + @MessagePattern({ cmd: 'polygon-create-keys' }) + async createSecp256k1KeyPair(payload: {orgId: string}): Promise { + return this.agentServiceService.createSecp256k1KeyPair(payload.orgId); + } + + @MessagePattern({ cmd: 'agent-create-connection-invitation' }) + async createConnectionInvitation(payload: { url: string; orgId: string; connectionPayload: ICreateConnectionInvitation }): Promise { + return this.agentServiceService.createConnectionInvitation(payload.url, payload.orgId, payload.connectionPayload); + } } diff --git a/apps/agent-service/src/agent-service.service.ts b/apps/agent-service/src/agent-service.service.ts index 4e4f540a3..e061e305e 100644 --- a/apps/agent-service/src/agent-service.service.ts +++ b/apps/agent-service/src/agent-service.service.ts @@ -19,8 +19,37 @@ import * as dotenv from 'dotenv'; import * as fs from 'fs'; import { map } from 'rxjs/operators'; dotenv.config(); -import { IGetCredDefAgentRedirection, IConnectionDetails, IUserRequestInterface, IAgentSpinupDto, IStoreOrgAgentDetails, ITenantCredDef, ITenantDto, ITenantSchema, IWalletProvision, ISendProofRequestPayload, IIssuanceCreateOffer, IOutOfBandCredentialOffer, IAgentSpinUpSatus, ICreateTenant, IAgentStatus, ICreateOrgAgent, IOrgAgentsResponse, IProofPresentation, IAgentProofRequest, IPresentation, IReceiveInvitationUrl, IReceiveInvitation } from './interface/agent-service.interface'; -import { AgentSpinUpStatus, AgentType, Ledgers, OrgAgentType } from '@credebl/enum/enum'; +import { + IGetCredDefAgentRedirection, + IConnectionDetails, + IUserRequestInterface, + IAgentSpinupDto, + IStoreOrgAgentDetails, + ITenantCredDef, + ITenantDto, + ITenantSchema, + IWalletProvision, + ISendProofRequestPayload, + IIssuanceCreateOffer, + IOutOfBandCredentialOffer, + IAgentSpinUpSatus, + ICreateTenant, + IAgentStatus, + ICreateOrgAgent, + IOrgAgentsResponse, + IProofPresentation, + IAgentProofRequest, + IPresentation, + IReceiveInvitationUrl, + IReceiveInvitation, + IQuestionPayload, + IDidCreate, + IWallet, + ITenantRecord, + LedgerListResponse, + ICreateConnectionInvitation +} from './interface/agent-service.interface'; +import { AgentSpinUpStatus, AgentType, DidMethod, Ledgers, OrgAgentType } from '@credebl/enum/enum'; import { AgentServiceRepository } from './repositories/agent-service.repository'; import { ledgers, org_agents, organisation, platform_config } from '@prisma/client'; import { CommonConstants } from '@credebl/common/common.constant'; @@ -34,13 +63,14 @@ import * as retry from 'async-retry'; import { Cache } from 'cache-manager'; import { CACHE_MANAGER } from '@nestjs/cache-manager'; import { IProofPresentationDetails } from '@credebl/common/interfaces/verification.interface'; -import { ICreateConnectionUrl } from '@credebl/common/interfaces/connection.interface'; import { IConnectionDetailsById } from 'apps/api-gateway/src/interfaces/IConnectionSearch.interface'; +import { ledgerName } from '@credebl/common/cast.helper'; +import { InvitationMessage } from '@credebl/common/interfaces/agent-service.interface'; +import * as CryptoJS from 'crypto-js'; @Injectable() @WebSocketGateway() export class AgentServiceService { - private readonly logger = new Logger('WalletService'); constructor( @@ -49,24 +79,21 @@ export class AgentServiceService { private readonly connectionService: ConnectionService, @Inject('NATS_CLIENT') private readonly agentServiceProxy: ClientProxy, @Inject(CACHE_MANAGER) private cacheService: Cache - ) { } + ) {} async ReplaceAt(input, search, replace, start, end): Promise { - return input.slice(0, start) - + input.slice(start, end).replace(search, replace) - + input.slice(end); + return input.slice(0, start) + input.slice(start, end).replace(search, replace) + input.slice(end); } /** * Spinup the agent by organization - * @param agentSpinupDto - * @param user + * @param agentSpinupDto + * @param user * @returns Get agent status */ async walletProvision(agentSpinupDto: IAgentSpinupDto, user: IUserRequestInterface): Promise { let agentProcess: ICreateOrgAgent; try { - // Invoke an internal function to create wallet await this.processWalletProvision(agentSpinupDto, user); const agentStatusResponse = { @@ -75,7 +102,6 @@ export class AgentServiceService { return agentStatusResponse; } catch (error) { - // Invoke an internal function to handle error to create wallet this.handleErrorOnWalletProvision(agentSpinupDto, error, agentProcess); throw new RpcException(error.response ? error.response : error); @@ -88,17 +114,16 @@ export class AgentServiceService { let agentProcess: ICreateOrgAgent; let getOrgAgent; try { - const [platformConfig, getAgentType, ledgerIdData] = await Promise.all([ this.agentServiceRepository.getPlatformConfigDetails(), this.agentServiceRepository.getAgentTypeDetails(), - this.agentServiceRepository.getLedgerDetails(agentSpinupDto.ledgerName ? agentSpinupDto.ledgerName : [Ledgers.Indicio_Demonet]) + this.agentServiceRepository.getLedgerDetails( + agentSpinupDto.ledgerName ? agentSpinupDto.ledgerName : [Ledgers.Indicio_Demonet] + ) ]); let orgData; if (!user?.userId && agentSpinupDto?.platformAdminEmail) { - - // Get Platform admin user by platform admin email platformAdminUser = await this.agentServiceRepository.getPlatfomAdminUser(agentSpinupDto?.platformAdminEmail); @@ -111,14 +136,12 @@ export class AgentServiceService { const platformAdminOrgDetails = await this.agentServiceRepository.getPlatfomOrg(agentSpinupDto?.orgName); if (agentSpinupDto.orgId) { - // Get organization details getOrgAgent = await this.agentServiceRepository.getAgentDetails(agentSpinupDto.orgId); // Get organization data by orgId orgData = await this.agentServiceRepository.getOrgDetails(agentSpinupDto.orgId); } else { - // Get platform organization details getOrgAgent = await this.agentServiceRepository.getAgentDetails(platformAdminOrgDetails); @@ -126,27 +149,28 @@ export class AgentServiceService { orgData = await this.agentServiceRepository.getOrgDetails(platformAdminOrgDetails); } - agentSpinupDto.ledgerId = agentSpinupDto.ledgerId?.length ? agentSpinupDto.ledgerId : ledgerIdData.map(ledger => ledger?.id); + agentSpinupDto.ledgerId = agentSpinupDto.ledgerId?.length + ? agentSpinupDto.ledgerId + : ledgerIdData.map((ledger) => ledger?.id); // Get genesis URL and ledger details const ledgerDetails = await this.agentServiceRepository.getGenesisUrl(agentSpinupDto.ledgerId); if (AgentSpinUpStatus.PROCESSED === getOrgAgent?.agentSpinUpStatus) { - throw new BadRequestException( - ResponseMessages.agent.error.walletAlreadyProcessing, - { cause: new Error(), description: ResponseMessages.errorMessages.badRequest } - ); + throw new BadRequestException(ResponseMessages.agent.error.walletAlreadyProcessing, { + cause: new Error(), + description: ResponseMessages.errorMessages.badRequest + }); } if (AgentSpinUpStatus.COMPLETED === getOrgAgent?.agentSpinUpStatus) { - throw new BadRequestException( - ResponseMessages.agent.error.walletAlreadyCreated, - { cause: new Error(), description: ResponseMessages.errorMessages.badRequest } - ); + throw new ConflictException(ResponseMessages.agent.error.walletAlreadyCreated, { + cause: new Error(), + description: ResponseMessages.errorMessages.conflict + }); } if (!agentSpinupDto.orgId) { - if (platformAdminOrgDetails) { agentSpinupDto.orgId = platformAdminOrgDetails; } @@ -154,8 +178,9 @@ export class AgentServiceService { agentSpinupDto.agentType = agentSpinupDto.agentType || getAgentType; agentSpinupDto.tenant = agentSpinupDto.tenant || false; - agentSpinupDto.ledgerName = agentSpinupDto.ledgerName?.length ? agentSpinupDto.ledgerName : [Ledgers.Indicio_Demonet]; - + agentSpinupDto.ledgerName = agentSpinupDto.ledgerName?.length + ? agentSpinupDto.ledgerName + : [Ledgers.Indicio_Demonet]; // Invoke function for validate platform configuration this.validatePlatformConfig(platformConfig); @@ -165,8 +190,14 @@ export class AgentServiceService { const apiEndpoint = platformConfig?.apiEndpoint; // Create payload for the wallet create and store payload - const walletProvisionPayload = await this.prepareWalletProvisionPayload(agentSpinupDto, externalIp, apiEndpoint, inboundEndpoint, ledgerDetails, orgData); - + const walletProvisionPayload = await this.prepareWalletProvisionPayload( + agentSpinupDto, + externalIp, + apiEndpoint, + inboundEndpoint, + ledgerDetails, + orgData + ); // Socket connection const socket: Socket = await this.initSocketConnection(`${process.env.SOCKET_HOST}`); @@ -176,8 +207,16 @@ export class AgentServiceService { agentProcess = await this.createOrgAgent(agentSpinUpStatus, userId); // AFJ agent spin-up - this._agentSpinup(walletProvisionPayload, agentSpinupDto, platformConfig?.sgApiKey, orgData, user, socket, agentSpinupDto.ledgerId, agentProcess); - + this._agentSpinup( + walletProvisionPayload, + agentSpinupDto, + platformConfig?.sgApiKey, + orgData, + user, + socket, + agentSpinupDto.ledgerId, + agentProcess + ); } catch (error) { this.handleErrorOnWalletProvision(agentSpinupDto, error, agentProcess); throw error; @@ -187,34 +226,34 @@ export class AgentServiceService { validatePlatformConfig(platformConfig: platform_config): void { if (!platformConfig) { this.logger.error(`Platform configuration is missing or invalid`); - throw new BadRequestException( - ResponseMessages.agent.error.platformConfiguration, - { cause: new Error(), description: ResponseMessages.errorMessages.badRequest } - ); + throw new BadRequestException(ResponseMessages.agent.error.platformConfiguration, { + cause: new Error(), + description: ResponseMessages.errorMessages.badRequest + }); } if (!platformConfig.apiEndpoint) { this.logger.error(`API endpoint is missing in the platform configuration`); - throw new BadRequestException( - ResponseMessages.agent.error.apiEndpoint, - { cause: new Error(), description: ResponseMessages.errorMessages.badRequest } - ); + throw new BadRequestException(ResponseMessages.agent.error.apiEndpoint, { + cause: new Error(), + description: ResponseMessages.errorMessages.badRequest + }); } if (!platformConfig.externalIp) { this.logger.error(`External IP is missing in the platform configuration`); - throw new BadRequestException( - ResponseMessages.agent.error.externalIp, - { cause: new Error(), description: ResponseMessages.errorMessages.badRequest } - ); + throw new BadRequestException(ResponseMessages.agent.error.externalIp, { + cause: new Error(), + description: ResponseMessages.errorMessages.badRequest + }); } if (typeof platformConfig.externalIp !== 'string') { this.logger.error(`External IP must be a string`); - throw new BadRequestException( - ResponseMessages.agent.error.externalIp, - { cause: new Error(), description: ResponseMessages.errorMessages.badRequest } - ); + throw new BadRequestException(ResponseMessages.agent.error.externalIp, { + cause: new Error(), + description: ResponseMessages.errorMessages.badRequest + }); } } @@ -222,10 +261,10 @@ export class AgentServiceService { try { if (!agentProcess) { this.logger.error(`Agent process is invalid or not in a completed state`); - throw new BadRequestException( - ResponseMessages.agent.error.externalIp, - { cause: new Error(), description: ResponseMessages.errorMessages.badRequest } - ); + throw new BadRequestException(ResponseMessages.agent.error.externalIp, { + cause: new Error(), + description: ResponseMessages.errorMessages.badRequest + }); } } catch (error) { this.logger.error(`Error validating agent process: ${error.message}`); @@ -236,12 +275,10 @@ export class AgentServiceService { emitAgentSpinupInitiatedEvent(agentSpinupDto: IAgentSpinupDto, socket: Socket): void { try { if (agentSpinupDto.clientSocketId) { - socket.emit('agent-spinup-process-initiated', { clientId: agentSpinupDto.clientSocketId }); // Log or perform other actions after emitting the event this.logger.log(`Agent spinup initiated event emitted for orgId ${agentSpinupDto.orgId}`); } - } catch (error) { this.logger.error(`Error emitting agent-spinup-initiated event: ${error.message}`); throw error; @@ -256,7 +293,7 @@ export class AgentServiceService { ledgerDetails: ledgers[], orgData: organisation ): Promise { - const ledgerArray = ledgerDetails.map(ledger => ({ + const ledgerArray = ledgerDetails.map((ledger) => ({ genesisTransactions: ledger.poolConfig, indyNamespace: ledger.indyNamespace })); @@ -306,13 +343,16 @@ export class AgentServiceService { this.logger.log(`Organization agent created with status: ${agentSpinUpStatus}`); return agentProcess; } catch (error) { - this.logger.error(`Error creating organization agent: ${error.message}`); throw error; } } - private async handleErrorOnWalletProvision(agentSpinupDto: IAgentSpinupDto, error: Error, agentProcess: ICreateOrgAgent): Promise { + private async handleErrorOnWalletProvision( + agentSpinupDto: IAgentSpinupDto, + error: Error, + agentProcess: ICreateOrgAgent + ): Promise { if (agentProcess) { const socket = await this.initSocketConnection(`${process.env.SOCKET_HOST}`); @@ -328,31 +368,52 @@ export class AgentServiceService { try { socket.emit('error-in-wallet-creation-process', { clientId: agentSpinupDto.clientSocketId, error }); this.logger.error(`Error in wallet creation process emitted for orgId ${agentSpinupDto.orgId}: ${error.message}`); - } catch (emitError) { this.logger.error(`Error emitting error-in-wallet-creation-process event: ${emitError.message}`); throw emitError; } } - async _agentSpinup(walletProvisionPayload: IWalletProvision, agentSpinupDto: IAgentSpinupDto, orgApiKey: string, orgData: organisation, user: IUserRequestInterface, socket: Socket, ledgerId: string[], agentProcess: ICreateOrgAgent): Promise { + async _agentSpinup( + walletProvisionPayload: IWalletProvision, + agentSpinupDto: IAgentSpinupDto, + orgApiKey: string, + orgData: organisation, + user: IUserRequestInterface, + socket: Socket, + ledgerId: string[], + agentProcess: ICreateOrgAgent + ): Promise { + let ledgerIdData = []; + try { + if (agentSpinupDto.method !== DidMethod.KEY && agentSpinupDto.method !== DidMethod.WEB) { + const { network } = agentSpinupDto; + const ledger = await ledgerName(network); + const ledgerList = (await this._getALlLedgerDetails()) as unknown as LedgerListResponse; + const isLedgerExist = ledgerList.response.find((existingLedgers) => existingLedgers.name === ledger); + if (!isLedgerExist) { + throw new BadRequestException(ResponseMessages.agent.error.invalidLedger, { + cause: new Error(), + description: ResponseMessages.errorMessages.notFound + }); + } + ledgerIdData = await this.agentServiceRepository.getLedgerDetails(ledger); + } /** - * Invoke wallet create and provision with agent + * Invoke wallet create and provision with agent */ const walletProvision = await this._walletProvision(walletProvisionPayload); - if (!walletProvision?.response) { this.logger.error(`Agent not able to spin-up`); - throw new BadRequestException( - ResponseMessages.agent.error.notAbleToSpinup, - { cause: new Error(), description: ResponseMessages.errorMessages.badRequest } - ); + throw new BadRequestException(ResponseMessages.agent.error.notAbleToSpinup, { + cause: new Error(), + description: ResponseMessages.errorMessages.badRequest + }); } const agentDetails = walletProvision.response; const agentEndPoint = `${process.env.API_GATEWAY_PROTOCOL}://${agentDetails.agentEndPoint}`; - /** * Socket connection */ @@ -364,27 +425,32 @@ export class AgentServiceService { socket.emit('invitation-url-creation-started', { clientId: agentSpinupDto.clientSocketId }); } + const encryptedToken = await this.tokenEncryption(agentDetails?.agentToken); + const agentPayload: IStoreOrgAgentDetails = { agentEndPoint, seed: agentSpinupDto.seed, - apiKey: agentDetails.agentToken, + apiKey: encryptedToken, agentsTypeId: agentSpinupDto?.agentType, orgId: orgData.id, walletName: agentSpinupDto.walletName, clientSocketId: agentSpinupDto.clientSocketId, - ledgerId, + method: agentSpinupDto.method, + role: agentSpinupDto.role, + network: agentSpinupDto.network, + keyType: agentSpinupDto.keyType, + ledgerId: ledgerIdData ? ledgerIdData.map((item) => item.id) : null, did: agentSpinupDto.did, id: agentProcess?.id }; - /** - * Store organization agent details + * Store organization agent details */ const storeAgentDetails = await this._storeOrgAgentDetails(agentPayload); - if (storeAgentDetails) { - - const filePath = `${process.cwd()}${process.env.AFJ_AGENT_TOKEN_PATH}${orgData.id}_${orgData.name.split(' ').join('_')}.json`; + const filePath = `${process.cwd()}${process.env.AFJ_AGENT_TOKEN_PATH}${orgData.id}_${orgData.name + .split(' ') + .join('_')}.json`; if (agentDetails?.agentToken) { fs.unlink(filePath, (err) => { if (err) { @@ -403,16 +469,15 @@ export class AgentServiceService { const getOrganization = await this.agentServiceRepository.getOrgDetails(orgData?.id); await this._createLegacyConnectionInvitation(orgData?.id, user, getOrganization.name); - if (agentSpinupDto.clientSocketId) { socket.emit('invitation-url-creation-success', { clientId: agentSpinupDto.clientSocketId }); } } else { this.logger.error(`Agent not able to spin-up`); - throw new BadRequestException( - ResponseMessages.agent.error.notAbleToSpinup, - { cause: new Error(), description: ResponseMessages.errorMessages.badRequest } - ); + throw new BadRequestException(ResponseMessages.agent.error.notAbleToSpinup, { + cause: new Error(), + description: ResponseMessages.errorMessages.badRequest + }); } } catch (error) { if (agentSpinupDto.clientSocketId) { @@ -421,7 +486,7 @@ export class AgentServiceService { if (agentProcess && agentProcess?.id) { /** - * If getting error remove organization agent + * If getting error remove organization agent */ await this.agentServiceRepository.removeOrgAgent(agentProcess?.id); } @@ -431,7 +496,6 @@ export class AgentServiceService { async _storeOrgAgentDetails(payload: IStoreOrgAgentDetails): Promise { try { - /** * Get orgaization agent type and agent details */ @@ -439,22 +503,18 @@ export class AgentServiceService { this._getAgentDid(payload), this.agentServiceRepository.getOrgAgentTypeDetails(OrgAgentType.DEDICATED) ]); - /** * Get DID method by agent */ const getDidMethod = await this._getDidMethod(payload, agentDid); - /** * Organization storage data */ - const storeOrgAgentData = await this._buildStoreOrgAgentData(payload, getDidMethod, orgAgentTypeId); - + const storeOrgAgentData = await this._buildStoreOrgAgentData(payload, getDidMethod, `${orgAgentTypeId}`); /** * Store org agent details */ const storeAgentDid = await this.agentServiceRepository.storeOrgAgentDetails(storeOrgAgentData); - return storeAgentDid; } catch (error) { await this._handleError(payload, error); @@ -462,22 +522,36 @@ export class AgentServiceService { } } - private async _getAgentDid(payload: IStoreOrgAgentDetails): Promise { - const { agentEndPoint, apiKey, seed, ledgerId, did } = payload; + const { agentEndPoint, apiKey, ledgerId, seed, keyType, method, network, role, did } = payload; const writeDid = 'write-did'; const ledgerDetails = await this.agentServiceRepository.getGenesisUrl(ledgerId); const agentDidWriteUrl = `${agentEndPoint}${CommonConstants.URL_AGENT_WRITE_DID}`; - return this._retryAgentSpinup(agentDidWriteUrl, apiKey, writeDid, seed, ledgerDetails[0].indyNamespace, did); + return this._retryAgentSpinup(agentDidWriteUrl, apiKey, writeDid, seed, keyType, method, network, role, did); } private async _getDidMethod(payload: IStoreOrgAgentDetails, agentDid: object): Promise { - const getDidDic = 'get-did-doc'; - const getDidMethodUrl = `${payload.agentEndPoint}${CommonConstants.URL_AGENT_GET_DID}`.replace('#', agentDid['did']); - return this._retryAgentSpinup(getDidMethodUrl, payload.apiKey, getDidDic); + const { agentEndPoint, apiKey, seed, keyType, method, network, role } = payload; + const getDidDoc = 'get-did-doc'; + const getDidMethodUrl = `${agentEndPoint}${CommonConstants.URL_AGENT_GET_DID}/${agentDid['did']}`; + return this._retryAgentSpinup( + getDidMethodUrl, + apiKey, + getDidDoc, + seed, + keyType, + method, + network, + role, + `${agentDid['did']}` + ); } - private _buildStoreOrgAgentData(payload: IStoreOrgAgentDetails, getDidMethod: object, orgAgentTypeId: string): IStoreOrgAgentDetails { + private _buildStoreOrgAgentData( + payload: IStoreOrgAgentDetails, + getDidMethod: object, + orgAgentTypeId: string + ): IStoreOrgAgentDetails { return { did: getDidMethod['didDocument']?.id, verkey: getDidMethod['didDocument']?.verificationMethod[0]?.publicKeyBase58, @@ -514,17 +588,31 @@ export class AgentServiceService { this.logger.error(`[_storeOrgAgentDetails] - Error in store agent details : ${JSON.stringify(error)}`); } - async _retryAgentSpinup(agentUrl: string, apiKey: string, agentApiState: string, seed?: string, indyNamespace?: string, did?: string): Promise { + async _retryAgentSpinup( + agentUrl: string, + apiKey: string, + agentApiState: string, + seed: string, + keyType: string, + method: string, + network: string, + role: string, + did: string + ): Promise { const retryOptions = { retries: 10 }; - try { + const getDcryptedToken = await this.commonService.decryptPassword(apiKey); return retry(async () => { if (agentApiState === 'write-did') { - return this.commonService.httpPost(agentUrl, { seed, method: indyNamespace, did }, { headers: { 'authorization': apiKey } }); + return this.commonService.httpPost( + agentUrl, + { seed, keyType, method, network, role, did }, + { headers: { authorization: getDcryptedToken } } + ); } else if (agentApiState === 'get-did-doc') { - return this.commonService.httpGet(agentUrl, { headers: { 'authorization': apiKey } }); + return this.commonService.httpGet(agentUrl, { headers: { authorization: getDcryptedToken } }); } }, retryOptions); } catch (error) { @@ -532,8 +620,11 @@ export class AgentServiceService { } } - - async _createLegacyConnectionInvitation(orgId: string, user: IUserRequestInterface, label: string): Promise<{ + async _createLegacyConnectionInvitation( + orgId: string, + user: IUserRequestInterface, + label: string + ): Promise<{ response; }> { try { @@ -541,27 +632,25 @@ export class AgentServiceService { cmd: 'create-connection' }; const payload = { orgId, user, label }; - return this.agentServiceProxy - .send(pattern, payload) - .pipe( - map((response) => ( - { - response - })) - ).toPromise() - .catch(error => { - this.logger.error(`catch: ${JSON.stringify(error)}`); - throw new HttpException( - { - status: error.statusCode, - error: error.message - }, error.error); - }); + return await this.natsCall(pattern, payload); } catch (error) { this.logger.error(`error in create-connection in wallet provision : ${JSON.stringify(error)}`); } } + async _getALlLedgerDetails(): Promise<{ + response; + }> { + try { + const pattern = { + cmd: 'get-all-ledgers' + }; + const payload = {}; + return await this.natsCall(pattern, payload); + } catch (error) { + this.logger.error(`error in while fetching all the ledger details : ${JSON.stringify(error)}`); + } + } async _walletProvision(payload: IWalletProvision): Promise<{ response; @@ -570,22 +659,7 @@ export class AgentServiceService { const pattern = { cmd: 'wallet-provisioning' }; - return this.agentServiceProxy - .send(pattern, payload) - .pipe( - map((response) => ( - { - response - })) - ).toPromise() - .catch(error => { - this.logger.error(`catch: ${JSON.stringify(error)}`); - throw new HttpException( - { - status: error.statusCode, - error: error.message - }, error.error); - }); + return await this.natsCall(pattern, payload); } catch (error) { this.logger.error(`error in wallet provision : ${JSON.stringify(error)}`); throw error; @@ -594,13 +668,12 @@ export class AgentServiceService { /** * Create tenant (Shared agent) - * @param payload - * @param user + * @param payload + * @param user * @returns Get agent status */ async createTenant(payload: ITenantDto, user: IUserRequestInterface): Promise { try { - const agentStatusResponse = { agentSpinupStatus: AgentSpinUpStatus.PROCESSED }; @@ -609,18 +682,18 @@ export class AgentServiceService { if (AgentSpinUpStatus.COMPLETED === getOrgAgent?.agentSpinUpStatus) { this.logger.error(`Your wallet is already been created.`); - throw new ConflictException( - ResponseMessages.agent.error.walletAlreadyCreated, - { cause: new Error(), description: ResponseMessages.errorMessages.conflict } - ); + throw new ConflictException(ResponseMessages.agent.error.walletAlreadyCreated, { + cause: new Error(), + description: ResponseMessages.errorMessages.conflict + }); } if (AgentSpinUpStatus.PROCESSED === getOrgAgent?.agentSpinUpStatus) { this.logger.error(`Your wallet is already processing.`); - throw new ConflictException( - ResponseMessages.agent.error.walletAlreadyProcessing, - { cause: new Error(), description: ResponseMessages.errorMessages.conflict } - ); + throw new ConflictException(ResponseMessages.agent.error.walletAlreadyProcessing, { + cause: new Error(), + description: ResponseMessages.errorMessages.conflict + }); } // Create tenant @@ -634,97 +707,88 @@ export class AgentServiceService { /** * Create tenant (Shared agent) - * @param payload - * @param user + * @param payload + * @param user * @returns Get agent status */ async _createTenant(payload: ITenantDto, user: IUserRequestInterface): Promise { let agentProcess; - + let ledgerIdData = []; try { + if (payload.method !== DidMethod.KEY && payload.method !== DidMethod.WEB) { + const { network } = payload; + const ledger = await ledgerName(network); + const ledgerList = (await this._getALlLedgerDetails()) as unknown as LedgerListResponse; + const isLedgerExist = ledgerList.response.find((existingLedgers) => existingLedgers.name === ledger); + if (!isLedgerExist) { + throw new BadRequestException(ResponseMessages.agent.error.invalidLedger, { + cause: new Error(), + description: ResponseMessages.errorMessages.notFound + }); + } - // Get orgaization agent details by orgId - const getOrgAgent = await this.agentServiceRepository.getAgentDetails(payload.orgId); - - if (AgentSpinUpStatus.COMPLETED === getOrgAgent?.agentSpinUpStatus) { - this.logger.error(`Your wallet has already been created.`); - throw new BadRequestException( - ResponseMessages.agent.error.walletAlreadyCreated, - { cause: new Error(), description: ResponseMessages.errorMessages.badRequest } - ); - } - - if (AgentSpinUpStatus.PROCESSED === getOrgAgent?.agentSpinUpStatus) { - this.logger.error(`Your wallet has already processing.`); - throw new BadRequestException( - ResponseMessages.agent.error.walletAlreadyProcessing, - { cause: new Error(), description: ResponseMessages.errorMessages.badRequest } - ); + ledgerIdData = await this.agentServiceRepository.getLedgerDetails(ledger); } - // Get ledgers details - const ledgerIdData = await this.agentServiceRepository.getLedgerDetails(Ledgers.Indicio_Demonet); - const ledgerIds = ledgerIdData.map(ledger => ledger?.id); - - payload.ledgerId = !payload.ledgerId || 0 === payload.ledgerId?.length ? ledgerIds : payload.ledgerId; const agentSpinUpStatus = AgentSpinUpStatus.PROCESSED; - // Create and stored agent details + // Create and stored agent details agentProcess = await this.agentServiceRepository.createOrgAgent(agentSpinUpStatus, user?.id); // Get platform admin details const platformAdminSpinnedUp = await this.getPlatformAdminAndNotify(payload.clientSocketId); - // Get genesis URLs by ledger Id - const ledgerDetails: ledgers[] = await this.agentServiceRepository.getGenesisUrl(payload.ledgerId); - - for (const iterator of ledgerDetails) { + payload.endpoint = platformAdminSpinnedUp.org_agents[0].agentEndPoint; + // Create tenant wallet and DID + const tenantDetails = await this.createTenantAndNotify(payload, platformAdminSpinnedUp); + if (!tenantDetails?.walletResponseDetails?.id || !tenantDetails?.DIDCreationOption?.did) { + this.logger.error(`Error in getting wallet id and wallet did`); + throw new NotFoundException(ResponseMessages.agent.error.notAbleToSpinUpAgent, { + cause: new Error(), + description: ResponseMessages.errorMessages.notFound + }); + } - // Create tenant in agent controller - const tenantDetails = await this.createTenantAndNotify(payload, iterator, platformAdminSpinnedUp); + if (AgentSpinUpStatus.COMPLETED !== platformAdminSpinnedUp.org_agents[0].agentSpinUpStatus) { + this.logger.error(`Platform-admin agent is not spun-up`); + throw new NotFoundException(ResponseMessages.agent.error.platformAdminNotAbleToSpinp, { + cause: new Error(), + description: ResponseMessages.errorMessages.notFound + }); + } + // Get shared agent type + const orgAgentTypeId = await this.agentServiceRepository.getOrgAgentTypeDetails(OrgAgentType.SHARED); + // Get agent type details + const agentTypeId = await this.agentServiceRepository.getAgentTypeId(AgentType.AFJ); + + const storeOrgAgentData: IStoreOrgAgentDetails = { + did: tenantDetails.DIDCreationOption.did, + isDidPublic: true, + didDoc: tenantDetails.DIDCreationOption.didDocument || tenantDetails.DIDCreationOption.didDoc, //changed the didDoc into didDocument + agentSpinUpStatus: AgentSpinUpStatus.COMPLETED, + agentsTypeId: agentTypeId, + orgId: payload.orgId, + agentEndPoint: platformAdminSpinnedUp.org_agents[0].agentEndPoint, + orgAgentTypeId, + tenantId: tenantDetails.walletResponseDetails['id'], + walletName: payload.label, + ledgerId: ledgerIdData ? ledgerIdData.map((item) => item.id) : null, + id: agentProcess?.id + }; - if (AgentSpinUpStatus.COMPLETED !== platformAdminSpinnedUp.org_agents[0].agentSpinUpStatus) { - this.logger.error(`Platform-admin agent is not spun-up`); - throw new NotFoundException( - ResponseMessages.agent.error.platformAdminNotAbleToSpinp, - { cause: new Error(), description: ResponseMessages.errorMessages.notFound } - ); - } + // Get organization data + const getOrganization = await this.agentServiceRepository.getOrgDetails(payload.orgId); - // Get org agent type details by shared agent - const orgAgentTypeId = await this.agentServiceRepository.getOrgAgentTypeDetails(OrgAgentType.SHARED); - - // Get agent type details by AFJ agent - const agentTypeId = await this.agentServiceRepository.getAgentTypeId(AgentType.AFJ); - - const storeOrgAgentData: IStoreOrgAgentDetails = { - did: tenantDetails['did'], - verkey: tenantDetails['verkey'], - isDidPublic: true, - agentSpinUpStatus: AgentSpinUpStatus.COMPLETED, - agentsTypeId: agentTypeId, - orgId: payload.orgId, - agentEndPoint: platformAdminSpinnedUp.org_agents[0].agentEndPoint, - orgAgentTypeId, - tenantId: tenantDetails['tenantRecord']['id'], - walletName: payload.label, - ledgerId: payload.ledgerId, - id: agentProcess?.id - }; + this.notifyClientSocket('agent-spinup-process-completed', payload.clientSocketId); - // Get organization data - const getOrganization = await this.agentServiceRepository.getOrgDetails(payload.orgId); + await this.agentServiceRepository.storeOrgAgentDetails(storeOrgAgentData); - this.notifyClientSocket('agent-spinup-process-completed', payload.clientSocketId); - await this.agentServiceRepository.storeOrgAgentDetails(storeOrgAgentData); + this.notifyClientSocket('invitation-url-creation-started', payload.clientSocketId); - this.notifyClientSocket('invitation-url-creation-started', payload.clientSocketId); + // Create the legacy connection invitation + await this._createLegacyConnectionInvitation(payload.orgId, user, getOrganization.name); - // Create the legacy connection invitation - this._createLegacyConnectionInvitation(payload.orgId, user, getOrganization.name); - - this.notifyClientSocket('invitation-url-creation-success', payload.clientSocketId); - } + this.notifyClientSocket('invitation-url-creation-success', payload.clientSocketId); } catch (error) { this.handleError(error, payload.clientSocketId); @@ -735,20 +799,113 @@ export class AgentServiceService { } } + /** + * Create wallet + * @param payload + * @returns wallet details + */ + async createWallet(payload: IWallet): Promise { + try { + const platformAdminSpinnedUp = await this.agentServiceRepository.platformAdminAgent( + CommonConstants.PLATFORM_ADMIN_ORG + ); + + const getPlatformAgentEndPoint = platformAdminSpinnedUp.org_agents[0].agentEndPoint; + const getDcryptedToken = await this.commonService.decryptPassword(platformAdminSpinnedUp?.org_agents[0].apiKey); + + const { label } = payload; + const createTenantOptions = { + config: { label } + }; + + const tenantDetails = await this.commonService.httpPost( + `${getPlatformAgentEndPoint}${CommonConstants.URL_SHAGENT_CREATE_TENANT}`, + createTenantOptions, + { headers: { authorization: getDcryptedToken } } + ); + + return tenantDetails; + } catch (error) { + this.logger.error(`error in create wallet : ${JSON.stringify(error)}`); + throw new RpcException(error.response ? error.response : error); + } + } + + /** + * Create did + * @param payload + * @returns did and didDocument + */ + async createDid(payload: IDidCreate, orgId: string, user: IUserRequestInterface): Promise { + try { + const agentDetails = await this.agentServiceRepository.getOrgAgentDetails(orgId); + + const getApiKey = await this.getOrgAgentApiKey(orgId); + const getOrgAgentType = await this.agentServiceRepository.getOrgAgentType(agentDetails?.orgAgentTypeId); + let url; + if (getOrgAgentType.agent === OrgAgentType.DEDICATED) { + url = `${agentDetails.agentEndPoint}${CommonConstants.URL_AGENT_WRITE_DID}`; + } else if (getOrgAgentType.agent === OrgAgentType.SHARED) { + url = `${agentDetails.agentEndPoint}${CommonConstants.URL_SHAGENT_CREATE_DID}${agentDetails.tenantId}`; + } + const didDetails = await this.commonService.httpPost(url, payload, { headers: { authorization: getApiKey } }); + return didDetails; + } catch (error) { + this.logger.error(`error in create did : ${JSON.stringify(error)}`); + + if (error?.response?.error?.message) { + throw new RpcException({ + statusCode: error?.response?.status, + error: error?.response?.error?.message + }); + } else { + throw new RpcException(error.response ? error.response : error); + } + } + } + + /** + * @returns Secp256k1 key pair for polygon DID + */ + async createSecp256k1KeyPair(orgId: string): Promise { + try { + const platformAdminSpinnedUp = await this.agentServiceRepository.platformAdminAgent( + CommonConstants.PLATFORM_ADMIN_ORG + ); + + const getPlatformAgentEndPoint = platformAdminSpinnedUp.org_agents[0].agentEndPoint; + const getDcryptedToken = await this.commonService.decryptPassword(platformAdminSpinnedUp?.org_agents[0].apiKey); + + const url = `${getPlatformAgentEndPoint}${CommonConstants.CREATE_POLYGON_SECP256k1_KEY}`; + + const createKeyPairResponse = await this.commonService.httpPost( + url, + {}, + { headers: { authorization: getDcryptedToken } } + ); + return createKeyPairResponse; + } catch (error) { + this.logger.error(`error in createSecp256k1KeyPair : ${JSON.stringify(error)}`); + throw new RpcException(error.response ? error.response : error); + } + } + private async getPlatformAdminAndNotify(clientSocketId: string | undefined): Promise { const socket = await this.createSocketInstance(); if (clientSocketId) { socket.emit('agent-spinup-process-initiated', { clientId: clientSocketId }); } - const platformAdminSpinnedUp = await this.agentServiceRepository.platformAdminAgent(CommonConstants.PLATFORM_ADMIN_ORG); + const platformAdminSpinnedUp = await this.agentServiceRepository.platformAdminAgent( + CommonConstants.PLATFORM_ADMIN_ORG + ); if (!platformAdminSpinnedUp) { this.logger.error(`Agent not able to spin-up`); - throw new BadRequestException( - ResponseMessages.agent.error.notAbleToSpinp, - { cause: new Error(), description: ResponseMessages.errorMessages.serverError } - ); + throw new BadRequestException(ResponseMessages.agent.error.notAbleToSpinp, { + cause: new Error(), + description: ResponseMessages.errorMessages.serverError + }); } return platformAdminSpinnedUp; @@ -756,36 +913,87 @@ export class AgentServiceService { /** * Create tenant on the agent - * @param payload - * @param ledgerIds - * @param platformAdminSpinnedUp + * @param payload + * @param ledgerIds + * @param platformAdminSpinnedUp * @returns Get tanant status */ - private async createTenantAndNotify(payload: ITenantDto, ledgerIds: ledgers, platformAdminSpinnedUp: IOrgAgentsResponse): Promise { + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private async createTenantAndNotify(payload: ITenantDto, platformAdminSpinnedUp: IOrgAgentsResponse): Promise { + const WalletSetupPayload = { ...payload }; const socket = await this.createSocketInstance(); - if (payload.clientSocketId) { - socket.emit('agent-spinup-process-initiated', { clientId: payload.clientSocketId }); + if (WalletSetupPayload.clientSocketId) { + socket.emit('agent-spinup-process-initiated', { clientId: WalletSetupPayload.clientSocketId }); } - - const { label, seed, did } = payload; - const createTenantOptions = { - config: { label }, - seed, - did: did ? did : undefined, - method: ledgerIds.indyNamespace + const walletLabel = WalletSetupPayload.label; + + delete WalletSetupPayload.label; + delete WalletSetupPayload.clientSocketId; + delete WalletSetupPayload.orgId; + delete WalletSetupPayload.ledgerId; + + const getDcryptedToken = await this.commonService.decryptPassword(platformAdminSpinnedUp?.org_agents[0].apiKey); + const walletResponseDetails = await this._createTenantWallet( + walletLabel, + platformAdminSpinnedUp.org_agents[0].agentEndPoint, + getDcryptedToken + ); + if (!walletResponseDetails && !walletResponseDetails.id) { + throw new InternalServerErrorException('Error while creating the wallet'); + } + const didCreateOption = { + didPayload: WalletSetupPayload, + agentEndpoint: platformAdminSpinnedUp.org_agents[0].agentEndPoint, + apiKey: getDcryptedToken, + tenantId: walletResponseDetails.id }; + const DIDCreationOption = await this._createDID(didCreateOption); + if (!DIDCreationOption) { + throw new InternalServerErrorException('Error while creating the wallet'); + } + return { walletResponseDetails, DIDCreationOption }; + } + // + + /** + * Create tenant wallet on the agent + * @param createTenantWalletPayload + * @returns Get tanant status + */ + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private async _createTenantWallet(label, endpoint, agentApiKey): Promise { + //remove any + const createTenantOptions = { + config: { label } + }; // Invoke an API request from the agent to create multi-tenant agent const tenantDetails = await this.commonService.httpPost( - `${platformAdminSpinnedUp.org_agents[0].agentEndPoint}${CommonConstants.URL_SHAGENT_CREATE_TENANT}`, + `${endpoint}${CommonConstants.URL_SHAGENT_CREATE_TENANT}`, createTenantOptions, - { headers: { 'authorization': platformAdminSpinnedUp.org_agents[0].apiKey } } + { headers: { authorization: agentApiKey } } ); - return tenantDetails; } + /** + * Create tenant wallet on the agent + * @param _createDID + * @returns Get tanant status + */ + private async _createDID(didCreateOption): Promise { + const { didPayload, agentEndpoint, apiKey, tenantId } = didCreateOption; + // Invoke an API request from the agent to create multi-tenant agent + const didDetails = await this.commonService.httpPost( + `${agentEndpoint}${CommonConstants.URL_SHAGENT_CREATE_DID}${tenantId}`, + didPayload, + { headers: { authorization: apiKey } } + ); + return didDetails; + } private async createSocketInstance(): Promise { return io(`${process.env.SOCKET_HOST}`, { reconnection: true, @@ -814,10 +1022,10 @@ export class AgentServiceService { async createSchema(payload: ITenantSchema): Promise { try { + const getApiKey = await this.getOrgAgentApiKey(payload.orgId); let schemaResponse; if (OrgAgentType.DEDICATED === payload.agentType) { - const url = `${payload.agentEndPoint}${CommonConstants.URL_SCHM_CREATE_SCHEMA}`; const schemaPayload = { attributes: payload.attributes, @@ -825,35 +1033,34 @@ export class AgentServiceService { name: payload.name, issuerId: payload.issuerId }; - schemaResponse = await this.commonService.httpPost(url, schemaPayload, { headers: { 'authorization': payload.apiKey } }) - .then(async (schema) => { - return schema; - }) - .catch(error => { - throw new InternalServerErrorException( - ResponseMessages.agent.error.agentDown, - { cause: new Error(), description: ResponseMessages.errorMessages.serverError } - ); + schemaResponse = await this.commonService + .httpPost(url, schemaPayload, { headers: { authorization: getApiKey } }) + .then(async (schema) => schema) + .catch((error) => { + throw new InternalServerErrorException(ResponseMessages.agent.error.agentDown, { + cause: new Error(), + description: ResponseMessages.errorMessages.serverError + }); }); - } else if (OrgAgentType.SHARED === payload.agentType) { - - const url = `${payload.agentEndPoint}${CommonConstants.URL_SHAGENT_CREATE_SCHEMA}`.replace('#', `${payload.tenantId}`); + const url = `${payload.agentEndPoint}${CommonConstants.URL_SHAGENT_CREATE_SCHEMA}`.replace( + '#', + `${payload.tenantId}` + ); const schemaPayload = { attributes: payload.payload.attributes, version: payload.payload.version, name: payload.payload.name, issuerId: payload.payload.issuerId }; - schemaResponse = await this.commonService.httpPost(url, schemaPayload, { headers: { 'authorization': payload.apiKey } }) - .then(async (schema) => { - return schema; - }) - .catch(error => { - throw new InternalServerErrorException( - ResponseMessages.agent.error.agentDown, - { cause: new Error(), description: ResponseMessages.errorMessages.serverError } - ); + schemaResponse = await this.commonService + .httpPost(url, schemaPayload, { headers: { authorization: getApiKey } }) + .then(async (schema) => schema) + .catch((error) => { + throw new InternalServerErrorException(ResponseMessages.agent.error.agentDown, { + cause: new Error(), + description: ResponseMessages.errorMessages.serverError + }); }); } return schemaResponse; @@ -867,20 +1074,21 @@ export class AgentServiceService { try { let schemaResponse; + const getApiKey = await this.getOrgAgentApiKey(payload.orgId); if (OrgAgentType.DEDICATED === payload.agentType) { - const url = `${payload.agentEndPoint}${CommonConstants.URL_SCHM_GET_SCHEMA_BY_ID.replace('#', `${payload.schemaId}`)}`; - schemaResponse = await this.commonService.httpGet(url, payload.schemaId) - .then(async (schema) => { - return schema; - }); - + const url = `${payload.agentEndPoint}${CommonConstants.URL_SCHM_GET_SCHEMA_BY_ID.replace( + '#', + `${payload.schemaId}` + )}`; + schemaResponse = await this.commonService.httpGet(url, payload.schemaId).then(async (schema) => schema); } else if (OrgAgentType.SHARED === payload.agentType) { - const url = `${payload.agentEndPoint}${CommonConstants.URL_SHAGENT_GET_SCHEMA}`.replace('@', `${payload.payload.schemaId}`).replace('#', `${payload.tenantId}`); + const url = `${payload.agentEndPoint}${CommonConstants.URL_SHAGENT_GET_SCHEMA}` + .replace('@', `${payload.payload.schemaId}`) + .replace('#', `${payload.tenantId}`); - schemaResponse = await this.commonService.httpGet(url, { headers: { 'authorization': payload.apiKey } }) - .then(async (schema) => { - return schema; - }); + schemaResponse = await this.commonService + .httpGet(url, { headers: { authorization: getApiKey } }) + .then(async (schema) => schema); } return schemaResponse; } catch (error) { @@ -893,8 +1101,8 @@ export class AgentServiceService { try { let credDefResponse; + const getApiKey = await this.getOrgAgentApiKey(payload.orgId); if (OrgAgentType.DEDICATED === String(payload.agentType)) { - const url = `${payload.agentEndPoint}${CommonConstants.URL_SCHM_CREATE_CRED_DEF}`; const credDefPayload = { tag: payload.tag, @@ -902,22 +1110,22 @@ export class AgentServiceService { issuerId: payload.issuerId }; - credDefResponse = await this.commonService.httpPost(url, credDefPayload, { headers: { 'authorization': payload.apiKey } }) - .then(async (credDef) => { - return credDef; - }); - + credDefResponse = await this.commonService + .httpPost(url, credDefPayload, { headers: { authorization: getApiKey } }) + .then(async (credDef) => credDef); } else if (OrgAgentType.SHARED === payload.agentType) { - const url = `${payload.agentEndPoint}${CommonConstants.URL_SHAGENT_CREATE_CRED_DEF}`.replace('#', `${payload.tenantId}`); + const url = `${payload.agentEndPoint}${CommonConstants.URL_SHAGENT_CREATE_CRED_DEF}`.replace( + '#', + `${payload.tenantId}` + ); const credDefPayload = { tag: payload.payload.tag, schemaId: payload.payload.schemaId, issuerId: payload.payload.issuerId }; - credDefResponse = await this.commonService.httpPost(url, credDefPayload, { headers: { 'authorization': payload.apiKey } }) - .then(async (credDef) => { - return credDef; - }); + credDefResponse = await this.commonService + .httpPost(url, credDefPayload, { headers: { authorization: getApiKey } }) + .then(async (credDef) => credDef); } return credDefResponse; @@ -931,19 +1139,22 @@ export class AgentServiceService { try { let credDefResponse; + const getApiKey = await this.getOrgAgentApiKey(payload.orgId); if (OrgAgentType.DEDICATED === payload.agentType) { - const url = `${payload.agentEndPoint}${CommonConstants.URL_SCHM_GET_CRED_DEF_BY_ID.replace('#', `${payload.credentialDefinitionId}`)}`; - credDefResponse = await this.commonService.httpGet(url, payload.credentialDefinitionId) - .then(async (credDef) => { - return credDef; - }); - + const url = `${payload.agentEndPoint}${CommonConstants.URL_SCHM_GET_CRED_DEF_BY_ID.replace( + '#', + `${payload.credentialDefinitionId}` + )}`; + credDefResponse = await this.commonService + .httpGet(url, { headers: { authorization: getApiKey } }) + .then(async (credDef) => credDef); } else if (OrgAgentType.SHARED === payload.agentType) { - const url = `${payload.agentEndPoint}${CommonConstants.URL_SHAGENT_GET_CRED_DEF}`.replace('@', `${payload.payload.credentialDefinitionId}`).replace('#', `${payload.tenantId}`); - credDefResponse = await this.commonService.httpGet(url, { headers: { 'authorization': payload.apiKey } }) - .then(async (credDef) => { - return credDef; - }); + const url = `${payload.agentEndPoint}${CommonConstants.URL_SHAGENT_GET_CRED_DEF}` + .replace('@', `${payload.payload.credentialDefinitionId}`) + .replace('#', `${payload.tenantId}`); + credDefResponse = await this.commonService + .httpGet(url, { headers: { authorization: getApiKey } }) + .then(async (credDef) => credDef); } return credDefResponse; } catch (error) { @@ -952,13 +1163,16 @@ export class AgentServiceService { } } - async createLegacyConnectionInvitation(connectionPayload: IConnectionDetails, url: string, apiKey: string): Promise { + async createLegacyConnectionInvitation( + connectionPayload: IConnectionDetails, + url: string, + orgId: string + ): Promise { try { - - + const getApiKey = await this.getOrgAgentApiKey(orgId); const data = await this.commonService - .httpPost(url, connectionPayload, { headers: { 'authorization': apiKey } }) - .then(async response => response); + .httpPost(url, connectionPayload, { headers: { authorization: getApiKey } }) + .then(async (response) => response); return data; } catch (error) { @@ -967,11 +1181,12 @@ export class AgentServiceService { } } - async sendCredentialCreateOffer(issueData: IIssuanceCreateOffer, url: string, apiKey: string): Promise { + async sendCredentialCreateOffer(issueData: IIssuanceCreateOffer, url: string, orgId: string): Promise { try { + const getApiKey = await this.getOrgAgentApiKey(orgId); const data = await this.commonService - .httpPost(url, issueData, { headers: { 'authorization': apiKey } }) - .then(async response => response); + .httpPost(url, issueData, { headers: { authorization: getApiKey } }) + .then(async (response) => response); return data; } catch (error) { this.logger.error(`Error in sendCredentialCreateOffer in agent service : ${JSON.stringify(error)}`); @@ -981,8 +1196,8 @@ export class AgentServiceService { async getProofPresentations(url: string, apiKey: string): Promise { try { const getProofPresentationsData = await this.commonService - .httpGet(url, { headers: { 'authorization': apiKey } }) - .then(async response => response); + .httpGet(url, { headers: { authorization: apiKey } }) + .then(async (response) => response); return getProofPresentationsData; } catch (error) { @@ -994,8 +1209,8 @@ export class AgentServiceService { async getIssueCredentials(url: string, apiKey: string): Promise { try { const data = await this.commonService - .httpGet(url, { headers: { 'authorization': apiKey } }) - .then(async response => response); + .httpGet(url, { headers: { authorization: apiKey } }) + .then(async (response) => response); return data; } catch (error) { this.logger.error(`Error in getIssueCredentials in agent service : ${JSON.stringify(error)}`); @@ -1003,13 +1218,9 @@ export class AgentServiceService { } } - async getProofPresentationById(url: string, apiKey: string): Promise { + async getProofPresentationById(url: string, orgId: string): Promise { try { - const getProofPresentationById = await this.commonService - .httpGet(url, { headers: { 'authorization': apiKey } }) - .then(async response => response) - .catch(error => this.handleAgentSpinupStatusErrors(error)); - + const getProofPresentationById = await this.agentCall(url, orgId); return getProofPresentationById; } catch (error) { this.logger.error(`Error in proof presentation by id in agent service : ${JSON.stringify(error)}`); @@ -1017,11 +1228,12 @@ export class AgentServiceService { } } - async getIssueCredentialsbyCredentialRecordId(url: string, apiKey: string): Promise { + async getIssueCredentialsbyCredentialRecordId(url: string, orgId: string): Promise { try { + const getApiKey = await this.getOrgAgentApiKey(orgId); const data = await this.commonService - .httpGet(url, { headers: { 'authorization': apiKey } }) - .then(async response => response); + .httpGet(url, { headers: { authorization: getApiKey } }) + .then(async (response) => response); return data; } catch (error) { this.logger.error(`Error in getIssueCredentialsbyCredentialRecordId in agent service : ${JSON.stringify(error)}`); @@ -1029,11 +1241,16 @@ export class AgentServiceService { } } - async sendProofRequest(proofRequestPayload: ISendProofRequestPayload, url: string, apiKey: string): Promise { + async sendProofRequest( + proofRequestPayload: ISendProofRequestPayload, + url: string, + orgId: string + ): Promise { try { + const getApiKey = await this.getOrgAgentApiKey(orgId); const sendProofRequest = await this.commonService - .httpPost(url, proofRequestPayload, { headers: { 'authorization': apiKey } }) - .then(async response => response); + .httpPost(url, proofRequestPayload, { headers: { authorization: getApiKey } }) + .then(async (response) => response); return sendProofRequest; } catch (error) { this.logger.error(`Error in send proof request in agent service : ${JSON.stringify(error)}`); @@ -1041,12 +1258,13 @@ export class AgentServiceService { } } - async verifyPresentation(url: string, apiKey: string): Promise { + async verifyPresentation(url: string, orgId: string): Promise { try { + const getApiKey = await this.getOrgAgentApiKey(orgId); const verifyPresentation = await this.commonService - .httpPost(url, '', { headers: { 'authorization': apiKey } }) - .then(async response => response) - .catch(error => this.handleAgentSpinupStatusErrors(error)); + .httpPost(url, '', { headers: { authorization: getApiKey } }) + .then(async (response) => response) + .catch((error) => this.handleAgentSpinupStatusErrors(error)); return verifyPresentation; } catch (error) { this.logger.error(`Error in verify proof presentation in agent service : ${JSON.stringify(error)}`); @@ -1054,11 +1272,12 @@ export class AgentServiceService { } } - async getConnections(url: string, apiKey: string): Promise { + async getConnections(url: string, orgId: string): Promise { try { + const getApiKey = await this.getOrgAgentApiKey(orgId); const data = await this.commonService - .httpGet(url, { headers: { 'authorization': apiKey } }) - .then(async response => response); + .httpGet(url, { headers: { authorization: getApiKey } }) + .then(async (response) => response); return data; } catch (error) { this.logger.error(`Error in getConnections in agent service : ${JSON.stringify(error)}`); @@ -1066,78 +1285,78 @@ export class AgentServiceService { } } - async getConnectionsByconnectionId(url: string, apiKey: string): Promise { - + async getConnectionsByconnectionId(url: string, orgId: string): Promise { try { - const data = await this.commonService - .httpGet(url, { headers: { 'authorization': apiKey } }) - .then(async response => response) - .catch(error => this.handleAgentSpinupStatusErrors(error)); - - return data; + const getConnectionsByconnectionId = await this.agentCall(url, orgId); + return getConnectionsByconnectionId; } catch (error) { this.logger.error(`Error in getConnectionsByconnectionId in agent service : ${JSON.stringify(error)}`); throw new RpcException(error.response ? error.response : error); } - } /** * Get agent health - * @param orgId + * @param orgId * @returns agent status */ async getAgentHealthDetails(orgId: string): Promise { try { - // Get organization agent details const orgAgentDetails: org_agents = await this.agentServiceRepository.getOrgAgentDetails(orgId); - let agentApiKey; if (orgAgentDetails) { - - agentApiKey = await this.cacheService.get(CommonConstants.CACHE_APIKEY_KEY); - if (!agentApiKey || null === agentApiKey || undefined === agentApiKey) { - agentApiKey = await this.getOrgAgentApiKey(orgId); - } - - if (agentApiKey === undefined || null) { - agentApiKey = await this.getOrgAgentApiKey(orgId); - } + agentApiKey = await this.getOrgAgentApiKey(orgId); } if (!orgAgentDetails) { - throw new NotFoundException( - ResponseMessages.agent.error.agentNotExists, - { cause: new Error(), description: ResponseMessages.errorMessages.notFound } - ); + throw new NotFoundException(ResponseMessages.agent.error.agentNotExists, { + cause: new Error(), + description: ResponseMessages.errorMessages.notFound + }); } if (!orgAgentDetails?.agentEndPoint) { - throw new NotFoundException( - ResponseMessages.agent.error.agentUrl, - { cause: new Error(), description: ResponseMessages.errorMessages.notFound } - ); + throw new NotFoundException(ResponseMessages.agent.error.agentUrl, { + cause: new Error(), + description: ResponseMessages.errorMessages.notFound + }); } // Invoke an API request from the agent to assess its current status const agentHealthData = await this.commonService - .httpGet(`${orgAgentDetails.agentEndPoint}${CommonConstants.URL_AGENT_STATUS}`, { headers: { 'authorization': agentApiKey } }) - .then(async response => response); + .httpGet(`${orgAgentDetails.agentEndPoint}${CommonConstants.URL_AGENT_STATUS}`, { + headers: { authorization: agentApiKey } + }) + .then(async (response) => response); return agentHealthData; - } catch (error) { this.logger.error(`Agent health details : ${JSON.stringify(error)}`); throw new RpcException(error.response ? error.response : error); } } - async sendOutOfBandProofRequest(proofRequestPayload: ISendProofRequestPayload, url: string, apiKey: string): Promise { + async getLedgerConfigDetails(user: IUserRequestInterface): Promise { try { + const getLedgerConfigData = await this.agentServiceRepository.getLedgerConfigByOrgId(); + return getLedgerConfigData; + } catch (error) { + this.logger.error(`Error in send out of band proof request in agent service : ${JSON.stringify(error)}`); + throw error; + } + } + + async sendOutOfBandProofRequest( + proofRequestPayload: ISendProofRequestPayload, + url: string, + orgId: string + ): Promise { + try { + const getApiKey = await this.getOrgAgentApiKey(orgId); const sendProofRequest = await this.commonService - .httpPost(url, proofRequestPayload, { headers: { 'authorization': apiKey } }) - .then(async response => response); + .httpPost(url, proofRequestPayload, { headers: { authorization: getApiKey } }) + .then(async (response) => response); return sendProofRequest; } catch (error) { this.logger.error(`Error in send out of band proof request in agent service : ${JSON.stringify(error)}`); @@ -1145,25 +1364,22 @@ export class AgentServiceService { } } - async getVerifiedProofDetails(url: string, apiKey: string): Promise { + async getVerifiedProofDetails(url: string, orgId: string): Promise { try { - const getVerifiedProofData = await this.commonService - .httpGet(url, { headers: { 'authorization': apiKey } }) - .then(async response => response) - .catch(error => this.handleAgentSpinupStatusErrors(error)); - - return getVerifiedProofData; + const getVerifiedProofDetails = await this.agentCall(url, orgId); + return getVerifiedProofDetails; } catch (error) { this.logger.error(`Error in get verified proof details in agent service : ${JSON.stringify(error)}`); throw new RpcException(error.response ? error.response : error); } } - async schemaEndorsementRequest(url: string, apiKey: string, requestSchemaPayload: object): Promise { + async schemaEndorsementRequest(url: string, orgId: string, requestSchemaPayload: object): Promise { try { + const getApiKey = await this.getOrgAgentApiKey(orgId); const schemaRequest = await this.commonService - .httpPost(url, requestSchemaPayload, { headers: { 'authorization': apiKey } }) - .then(async response => response); + .httpPost(url, requestSchemaPayload, { headers: { authorization: getApiKey } }) + .then(async (response) => response); return schemaRequest; } catch (error) { this.logger.error(`Error in schema endorsement request in agent service : ${JSON.stringify(error)}`); @@ -1171,23 +1387,27 @@ export class AgentServiceService { } } - async credDefEndorsementRequest(url: string, apiKey: string, requestSchemaPayload: object): Promise { + async credDefEndorsementRequest(url: string, orgId: string, requestSchemaPayload: object): Promise { try { + const getApiKey = await this.getOrgAgentApiKey(orgId); const credDefRequest = await this.commonService - .httpPost(url, requestSchemaPayload, { headers: { 'authorization': apiKey } }) - .then(async response => response); + .httpPost(url, requestSchemaPayload, { headers: { authorization: getApiKey } }) + .then(async (response) => response); return credDefRequest; } catch (error) { - this.logger.error(`Error in credential-definition endorsement request in agent service : ${JSON.stringify(error)}`); + this.logger.error( + `Error in credential-definition endorsement request in agent service : ${JSON.stringify(error)}` + ); throw error; } } - async signTransaction(url: string, apiKey: string, signEndorsementPayload: object): Promise { + async signTransaction(url: string, orgId: string, signEndorsementPayload: object): Promise { try { + const getApiKey = await this.getOrgAgentApiKey(orgId); const signEndorsementTransaction = await this.commonService - .httpPost(url, signEndorsementPayload, { headers: { 'authorization': apiKey } }) - .then(async response => response); + .httpPost(url, signEndorsementPayload, { headers: { authorization: getApiKey } }) + .then(async (response) => response); return signEndorsementTransaction; } catch (error) { @@ -1196,12 +1416,12 @@ export class AgentServiceService { } } - async sumbitTransaction(url: string, apiKey: string, submitEndorsementPayload: object): Promise { + async sumbitTransaction(url: string, orgId: string, submitEndorsementPayload: object): Promise { try { - + const getApiKey = await this.getOrgAgentApiKey(orgId); const signEndorsementTransaction = await this.commonService - .httpPost(url, submitEndorsementPayload, { headers: { 'authorization': apiKey } }) - .then(async response => response); + .httpPost(url, submitEndorsementPayload, { headers: { authorization: getApiKey } }) + .then(async (response) => response); return signEndorsementTransaction; } catch (error) { @@ -1210,11 +1430,16 @@ export class AgentServiceService { } } - async outOfBandCredentialOffer(outOfBandIssuancePayload: IOutOfBandCredentialOffer, url: string, apiKey: string): Promise { + async outOfBandCredentialOffer( + outOfBandIssuancePayload: IOutOfBandCredentialOffer, + url: string, + orgId: string + ): Promise { try { + const getApiKey = await this.getOrgAgentApiKey(orgId); const sendOutOfbandCredentialOffer = await this.commonService - .httpPost(url, outOfBandIssuancePayload, { headers: { 'authorization': apiKey } }) - .then(async response => response); + .httpPost(url, outOfBandIssuancePayload, { headers: { authorization: getApiKey } }) + .then(async (response) => response); return sendOutOfbandCredentialOffer; } catch (error) { this.logger.error(`Error in out-of-band credential in agent service : ${JSON.stringify(error)}`); @@ -1222,14 +1447,11 @@ export class AgentServiceService { } } - async deleteWallet( - url: string, - apiKey: string - ): Promise { + async deleteWallet(url: string, apiKey: string): Promise { try { const deleteWallet = await this.commonService - .httpDelete(url, { headers: { 'authorization': apiKey } }) - .then(async response => response); + .httpDelete(url, { headers: { authorization: apiKey } }) + .then(async (response) => response); return deleteWallet; } catch (error) { this.logger.error(`Error in delete wallet in agent service : ${JSON.stringify(error)}`); @@ -1237,11 +1459,12 @@ export class AgentServiceService { } } - async receiveInvitationUrl(receiveInvitationUrl: IReceiveInvitationUrl, url: string, apiKey: string): Promise { + async receiveInvitationUrl(receiveInvitationUrl: IReceiveInvitationUrl, url: string, orgId: string): Promise { try { + const getApiKey = await this.getOrgAgentApiKey(orgId); const receiveInvitationUrlRes = await this.commonService - .httpPost(url, receiveInvitationUrl, { headers: { 'authorization': apiKey } }) - .then(async response => response); + .httpPost(url, receiveInvitationUrl, { headers: { authorization: getApiKey } }) + .then(async (response) => response); return receiveInvitationUrlRes; } catch (error) { this.logger.error(`Error in receive invitation in agent service : ${JSON.stringify(error)}`); @@ -1249,11 +1472,12 @@ export class AgentServiceService { } } - async receiveInvitation(receiveInvitation: IReceiveInvitation, url: string, apiKey: string): Promise { + async receiveInvitation(receiveInvitation: IReceiveInvitation, url: string, orgId: string): Promise { try { + const getApiKey = await this.getOrgAgentApiKey(orgId); const receiveInvitationRes = await this.commonService - .httpPost(url, receiveInvitation, { headers: { 'authorization': apiKey } }) - .then(async response => response); + .httpPost(url, receiveInvitation, { headers: { authorization: getApiKey } }) + .then(async (response) => response); return receiveInvitationRes; } catch (error) { this.logger.error(`Error in receive invitation in agent service : ${JSON.stringify(error)}`); @@ -1263,47 +1487,140 @@ export class AgentServiceService { async getOrgAgentApiKey(orgId: string): Promise { try { - let agentApiKey; - const orgAgentApiKey = await this.agentServiceRepository.getAgentApiKey(orgId); + const orgAgentApiKey = await this.agentServiceRepository.getAgentApiKey(orgId); + const orgAgentId = await this.agentServiceRepository.getOrgAgentTypeDetails(OrgAgentType.SHARED); + const cacheKey = orgAgentApiKey?.orgAgentTypeId === orgAgentId ? CommonConstants.CACHE_SHARED_APIKEY_KEY : CommonConstants.CACHE_APIKEY_KEY; + + let apiKey = await this.cacheService.get(cacheKey); + if (!apiKey) { + if (orgAgentApiKey?.orgAgentTypeId === orgAgentId) { + const platformAdminSpinnedUp = await this.agentServiceRepository.platformAdminAgent(CommonConstants.PLATFORM_ADMIN_ORG); + if (!platformAdminSpinnedUp) { + throw new InternalServerErrorException('Agent not able to spin-up'); + } + apiKey = platformAdminSpinnedUp.org_agents[0]?.apiKey; + } else { + apiKey = orgAgentApiKey?.apiKey; + } + if (!apiKey) { + throw new NotFoundException(ResponseMessages.agent.error.apiKeyNotExist); + } + await this.cacheService.set(cacheKey, apiKey, 0); + } + + const decryptedToken = await this.commonService.decryptPassword(apiKey); + return decryptedToken; + } catch (error) { + this.logger.error(`Agent api key details : ${JSON.stringify(error)}`); + throw error; + } +} - const orgAgentId = await this.agentServiceRepository.getOrgAgentTypeDetails(OrgAgentType.SHARED); - if (orgAgentApiKey?.orgAgentTypeId === orgAgentId) { - const platformAdminSpinnedUp = await this.agentServiceRepository.platformAdminAgent(CommonConstants.PLATFORM_ADMIN_ORG); - - const [orgAgentData] = platformAdminSpinnedUp.org_agents; - const { apiKey } = orgAgentData; - if (!platformAdminSpinnedUp) { - throw new InternalServerErrorException('Agent not able to spin-up'); - } + async handleAgentSpinupStatusErrors(error: string): Promise { + if (error && Object.keys(error).length === 0) { + throw new InternalServerErrorException(ResponseMessages.agent.error.agentDown, { + cause: new Error(), + description: ResponseMessages.errorMessages.serverError + }); + } else { + throw error; + } + } - agentApiKey = apiKey; + async sendQuestion(questionPayload: IQuestionPayload, url: string, orgId: string): Promise { + try { + const getApiKey = await this.getOrgAgentApiKey(orgId); + const sendQuestionRes = await this.commonService + .httpPost(url, questionPayload, { headers: { authorization: getApiKey } }) + .then(async (response) => response); + return sendQuestionRes; + } catch (error) { + this.logger.error(`Error in send question in agent service : ${JSON.stringify(error)}`); + throw error; + } + } - } else { - agentApiKey = orgAgentApiKey?.apiKey; - } + async getQuestionAnswersRecord(url: string, orgId: string): Promise { + try { + const getQuestionAnswersRecord = await this.agentCall(url, orgId); + return getQuestionAnswersRecord; + } catch (error) { + this.logger.error(`Error in getQuestionAnswersRecord in agent service : ${JSON.stringify(error)}`); + throw new RpcException(error.response ? error.response : error); + } + } - if (!agentApiKey) { - throw new NotFoundException(ResponseMessages.agent.error.apiKeyNotExist); - } - await this.cacheService.set(CommonConstants.CACHE_APIKEY_KEY, agentApiKey, CommonConstants.CACHE_TTL_SECONDS); - return agentApiKey; + async agentCall(url: string, orgId: string): Promise { + const getApiKey = await this.getOrgAgentApiKey(orgId); + + const data = await this.commonService + .httpGet(url, { headers: { authorization: getApiKey } }) + .then(async (response) => response) + .catch((error) => this.handleAgentSpinupStatusErrors(error)); + + return data; + } + + async createW3CSchema(url: string, orgId: string, schemaRequestPayload): Promise { + try { + const getApiKey = await this.getOrgAgentApiKey(orgId); + const schemaRequest = await this.commonService + .httpPost(url, schemaRequestPayload, { headers: { 'authorization': getApiKey } }) + .then(async response => response); + return schemaRequest; + } catch (error) { + this.logger.error(`Error in createW3CSchema request in agent service : ${JSON.stringify(error)}`); + } + } + + async createConnectionInvitation(url: string, orgId: string, connectionPayload: ICreateConnectionInvitation): Promise { + try { + const getApiKey = await this.getOrgAgentApiKey(orgId); + const createConnectionInvitation = await this.commonService + .httpPost(url, connectionPayload, { headers: { authorization: getApiKey } }) + .then(async (response) => response); + return createConnectionInvitation; } catch (error) { - this.logger.error(`Agent api key details : ${JSON.stringify(error)}`); + this.logger.error(`Error in create connection invitation in agent service : ${JSON.stringify(error)}`); throw error; } } - async handleAgentSpinupStatusErrors(error: string): Promise { - if (error && Object.keys(error).length === 0) { - throw new InternalServerErrorException( - ResponseMessages.agent.error.agentDown, - { cause: new Error(), description: ResponseMessages.errorMessages.serverError } - ); - } else { + async natsCall(pattern: object, payload: object): Promise<{ + response: string; + }> { + try { + return this.agentServiceProxy + .send(pattern, payload) + .pipe( + map((response) => ( + { + response + })) + ).toPromise() + .catch(error => { + this.logger.error(`catch: ${JSON.stringify(error)}`); + throw new HttpException( + { + status: error.statusCode, + error: error.message + }, error.error); + }); + } catch (error) { + this.logger.error(`[natsCall] - error in nats call : ${JSON.stringify(error)}`); throw error; } - } -} + } + + private async tokenEncryption(token: string): Promise { + try { + const encryptedToken = CryptoJS.AES.encrypt(JSON.stringify(token), process.env.CRYPTO_PRIVATE_KEY).toString(); + return encryptedToken; + } catch (error) { + throw error; + } + } +} \ No newline at end of file diff --git a/apps/agent-service/src/interface/agent-service.interface.ts b/apps/agent-service/src/interface/agent-service.interface.ts index 685122c49..38f5f9388 100644 --- a/apps/agent-service/src/interface/agent-service.interface.ts +++ b/apps/agent-service/src/interface/agent-service.interface.ts @@ -1,415 +1,531 @@ import { UserRoleOrgPermsDto } from 'apps/api-gateway/src/dtos/user-role-org-perms.dto'; export interface IAgentSpinupDto { - - walletName: string; - walletPassword: string; - seed: string; - orgId?: string; - orgName?: string; - ledgerId?: string[]; - did?: string; - agentType?: string; - transactionApproval?: boolean; - clientSocketId?: string - tenant?: boolean; - ledgerName?: string[]; - platformAdminEmail?: string; - apiKey?: string; + walletName: string; + walletPassword: string; + seed: string; + orgId?: string; + orgName?: string; + ledgerId?: string[]; + keyType: string; + domain?: string; + privatekey?: string; + endpoint?: string; + role?: string; + network?: string; + endorserDid?: string; + method: string; + did?: string; + agentType?: string; + transactionApproval?: boolean; + clientSocketId?: string; + tenant?: boolean; + ledgerName?: string[]; + platformAdminEmail?: string; + apiKey?: string; } export interface IOutOfBandCredentialOffer { - emailId: string; - attributes: IAttributes[]; - credentialDefinitionId: string; - comment: string; - protocolVersion?: string; - orgId: string; - goalCode?: string, - parentThreadId?: string, - willConfirm?: boolean, - label?: string + emailId: string; + attributes: IAttributes[]; + credentialDefinitionId: string; + comment: string; + protocolVersion?: string; + orgId: string; + goalCode?: string; + parentThreadId?: string; + willConfirm?: boolean; + label?: string; } export interface ITenantDto { - label: string; - seed: string; - ledgerId?: string[]; - method: string; - orgId: string; - did?: string; - tenantId?: string; - clientSocketId?: string; + label: string; + seed?: string; + keyType: string; + ledgerId: string[]; + domain?: string; + privatekey?: string; + endpoint?: string; + role?: string; + network?: string; + endorserDid?: string; + method: string; + orgId: string; + did?: string; + tenantId?: string; + didDocument?: string; + clientSocketId?: string; +} + +export interface IWallet { + label: string; + orgId: string; + did?: string; + clientSocketId?: string; +} + +export interface IDidCreate { + keyType: KeyType; + seed: string; + domain?: string; + network?: string; + privatekey?: string; + endpoint?: string; + method: string; + did?: string; + role?: string; + endorserDid?: string; } - export interface ITenantSchema { - tenantId?: string; - attributes: string[]; - version: string; - name: string; - issuerId?: string; - payload?: ITenantSchemaDto; - method?: string; - agentType?: string; - apiKey?: string; - agentEndPoint?: string; + tenantId?: string; + attributes: string[]; + version: string; + name: string; + issuerId?: string; + payload?: ITenantSchemaDto; + method?: string; + agentType?: string; + apiKey?: string; + agentEndPoint?: string; + orgId?: string; } export interface ITenantSchemaDto { - attributes: string[]; - version: string; - name: string; - issuerId: string; + attributes: string[]; + version: string; + name: string; + issuerId: string; } export interface IGetSchemaAgentRedirection { - schemaId?: string; - tenantId?: string; - payload?: IGetSchemaFromTenantPayload; - apiKey?: string; - agentEndPoint?: string; - agentType?: string; - method?: string; + schemaId?: string; + tenantId?: string; + payload?: IGetSchemaFromTenantPayload; + apiKey?: string; + agentEndPoint?: string; + agentType?: string; + method?: string; + orgId?: string; } export interface IGetSchemaFromTenantPayload { - schemaId: string; + schemaId: string; } export interface ITenantCredDef { - tenantId?: string; - tag?: string; - schemaId?: string; - issuerId?: string; - payload?: ITenantCredDef; - method?: string; - agentType?: string; - apiKey?: string; - agentEndPoint?: string; + tenantId?: string; + tag?: string; + schemaId?: string; + issuerId?: string; + payload?: ITenantCredDef; + method?: string; + agentType?: string; + apiKey?: string; + agentEndPoint?: string; + orgId?: string; } export interface IWalletProvision { - orgId: string; - externalIp: string; - walletName: string; - walletPassword: string; - seed: string; - webhookEndpoint: string; - walletStorageHost: string; - walletStoragePort: string; - walletStorageUser: string; - walletStoragePassword: string; - containerName: string; - agentType: string; - orgName: string; - indyLedger: string; - afjVersion: string; - protocol: string; - tenant: boolean; - inboundEndpoint: string; - apiKey?: string; + orgId: string; + externalIp: string; + walletName: string; + walletPassword: string; + seed: string; + webhookEndpoint: string; + walletStorageHost: string; + walletStoragePort: string; + walletStorageUser: string; + walletStoragePassword: string; + containerName: string; + agentType: string; + orgName: string; + indyLedger: string; + afjVersion: string; + protocol: string; + tenant: boolean; + inboundEndpoint: string; + apiKey?: string; } export interface IPlatformConfigDto { - externalIP: string; - genesisURL: string; - adminKey: string; - lastInternalIP: string; - platformTestNetApiKey: string; - sgEmailFrom: string; - apiEndpoint: string; - tailsFileServer: string; + externalIP: string; + genesisURL: string; + adminKey: string; + lastInternalIP: string; + platformTestNetApiKey: string; + sgEmailFrom: string; + apiEndpoint: string; + tailsFileServer: string; } export interface IStoreOrgAgentDetails { - id?: string; - clientSocketId?: string; - agentEndPoint?: string; - apiKey?: string; - seed?: string; - did?: string; - verkey?: string; - isDidPublic?: boolean; - agentSpinUpStatus?: number; - walletName?: string; - agentsTypeId?: string; - orgId?: string; - agentId?: string; - orgAgentTypeId?: string; - tenantId?: string; - ledgerId?: string[]; - agentType?: string; + id?: string; + clientSocketId?: string; + agentEndPoint?: string; + apiKey?: string; + seed?: string; + keyType?: string; + method?: string; + network?: string; + role?: string; + did?: string; + didDoc?: string; + verkey?: string; + isDidPublic?: boolean; + agentSpinUpStatus?: number; + walletName?: string; + agentsTypeId?: string; + orgId?: string; + agentId?: string; + orgAgentTypeId?: string; + tenantId?: string; + ledgerId?: string[]; + agentType?: string; +} + +export interface IStoreOrgAgent { + id?: string; + clientSocketId?: string; + agentEndPoint?: string; + apiKey?: string; + seed?: string; + did?: string; + verkey?: string; + isDidPublic?: boolean; + agentSpinUpStatus?: number; + walletName?: string; + agentsTypeId?: string; + orgId?: string; + agentId?: string; + orgAgentTypeId?: string; + tenantId?: string; + ledgerId?: unknown; + agentType?: string; } - export interface IConnectionDetails { - multiUseInvitation?: boolean; - autoAcceptConnection: boolean; + multiUseInvitation?: boolean; + autoAcceptConnection: boolean; } export interface IUserRequestInterface { - userId?: string; - id?: string; - email: string; - orgId: string; - agentEndPoint?: string; - apiKey?: string; - tenantId?: string; - tenantName?: string; - tenantOrgId?: string; - userRoleOrgPermissions?: UserRoleOrgPermsDto[]; - orgName?: string; - selectedOrg: IOrgInterface; + userId?: string; + id?: string; + email: string; + orgId: string; + agentEndPoint?: string; + apiKey?: string; + tenantId?: string; + tenantName?: string; + tenantOrgId?: string; + userRoleOrgPermissions?: UserRoleOrgPermsDto[]; + orgName?: string; + selectedOrg: IOrgInterface; } export interface IOrgInterface { - id: string; - userId: string; - orgRoleId: string; - orgId: string; - orgRole: object; - organisation: IOrganizationAgentInterface; + id: string; + userId: string; + orgRoleId: string; + orgId: string; + orgRole: object; + organisation: IOrganizationAgentInterface; } export interface IOrganizationAgentInterface { - name: string; - description: string; - org_agents: IOrgAgentInterface[] + name: string; + description: string; + org_agents: IOrgAgentInterface[]; +} +export interface IPlatformAgent { + seed: string; + keyType: string; + method: string; + network: string; + role: string; } export interface IOrgAgentInterface { - orgDid: string; - verkey: string; - agentEndPoint: string; - agentOptions: string; - walletName: string; - agentsTypeId: string; - orgId: string; + orgDid: string; + verkey: string; + agentEndPoint: string; + agentOptions: string; + walletName: string; + agentsTypeId: string; + orgId: string; } export interface ITenantCredDefDto { - tag: string; - schemaId: string; - issuerId: string; + tag: string; + schemaId: string; + issuerId: string; } export interface IGetCredDefAgentRedirection { - credentialDefinitionId?: string; - tenantId?: string; - payload?: IGetCredDefFromTenantPayload; - apiKey?: string; - agentEndPoint?: string; - agentType?: string; - method?: string; + credentialDefinitionId?: string; + tenantId?: string; + payload?: IGetCredDefFromTenantPayload; + apiKey?: string; + agentEndPoint?: string; + agentType?: string; + method?: string; + orgId?: string; } export interface IGetCredDefFromTenantPayload { - credentialDefinitionId: string; + credentialDefinitionId: string; } export interface IIssuanceCreateOffer { - connectionId: string; - credentialFormats: ICredentialFormats; - autoAcceptCredential: string; - comment: string; + connectionId: string; + credentialFormats: ICredentialFormats; + autoAcceptCredential: string; + comment: string; } export interface ICredentialFormats { - indy: IIndy; - credentialDefinitionId: string; + indy: IIndy; + credentialDefinitionId: string; } export interface IIndy { - attributes: IAttributes[]; + attributes: IAttributes[]; } export interface IAttributes { - name: string; - value: string; + name: string; + value: string; } export interface ISendProofRequestPayload { - comment: string; - connectionId?: string; - proofFormats: IProofFormats; - autoAcceptProof: string; - goalCode?: string; - parentThreadId?: string; - willConfirm?: boolean; - protocolVersion?: string; + comment: string; + connectionId?: string; + proofFormats: IProofFormats; + autoAcceptProof: string; + goalCode?: string; + parentThreadId?: string; + willConfirm?: boolean; + protocolVersion?: string; } export interface IAgentStatus { - label: string; - endpoints: string[]; - isInitialized: boolean; + label: string; + endpoints: string[]; + isInitialized: boolean; } interface IProofFormats { - indy: IndyProof + indy: IndyProof; } interface IndyProof { - name: string; - version: string; - requested_attributes: IRequestedAttributes; - requested_predicates: IRequestedPredicates; + name: string; + version: string; + requested_attributes: IRequestedAttributes; + requested_predicates: IRequestedPredicates; } interface IRequestedAttributes { - [key: string]: IRequestedAttributesName; + [key: string]: IRequestedAttributesName; } interface IRequestedAttributesName { - name: string; - restrictions: IRequestedRestriction[] + name: string; + restrictions: IRequestedRestriction[]; } interface IRequestedPredicates { - [key: string]: IRequestedPredicatesName; + [key: string]: IRequestedPredicatesName; } interface IRequestedPredicatesName { - name: string; - restrictions: IRequestedRestriction[] + name: string; + restrictions: IRequestedRestriction[]; } interface IRequestedRestriction { - cred_def_id?: string; - schema_id?: string; + cred_def_id?: string; + schema_id?: string; + schema_issuer_did?: string; + schema_name?: string; + issuer_did?: string; } export interface IAgentSpinUpSatus { - agentSpinupStatus: number; + agentSpinupStatus: number; } interface IWalletConfig { - id: string; - key: string; - keyDerivationMethod: string; + id: string; + key: string; + keyDerivationMethod: string; } interface IConfig { - label: string; - walletConfig: IWalletConfig; + label: string; + walletConfig: IWalletConfig; } - -interface ITenantRecord { - _tags: string; - metadata: string; - id: string; - createdAt: string; - config: IConfig; - updatedAt: string; +export interface ITenantRecord { + _tags: string; + metadata: string; + id: string; + createdAt: string; + config: IConfig; + updatedAt: string; } export interface ICreateTenant { - tenantRecord: ITenantRecord; - did: string; - verkey: string; + tenantRecord: ITenantRecord; + did: string; + verkey: string; } export interface IOrgAgent { - agentSpinUpStatus: number; + agentSpinUpStatus: number; } export interface IOrgLedgers { - id: string; + id: string; } export interface ICreateOrgAgent { - id: string; + id: string; } interface IOrgAgentEndPoint { - agentSpinUpStatus: number; - agentEndPoint: string; - apiKey; + agentSpinUpStatus: number; + agentEndPoint: string; + apiKey; } export interface IOrgAgentsResponse { - org_agents: IOrgAgentEndPoint[]; + org_agents: IOrgAgentEndPoint[]; } export interface IStoreAgent { - id: string; + id: string; } export interface IAcceptCredentials { - credentialRecordId: string; + credentialRecordId: string; } export interface IAgentProofRequest { - metadata: object; - id: string; - createdAt: string; - protocolVersion: string; - state: string; - connectionId: string; - threadId: string; - autoAcceptProof: string; - updatedAt: string; + metadata: object; + id: string; + createdAt: string; + protocolVersion: string; + state: string; + connectionId: string; + threadId: string; + autoAcceptProof: string; + updatedAt: string; } export interface IPresentation { - _tags: ITags; - metadata: object; - id: string; + _tags: ITags; + metadata: object; + id: string; } export interface IStoreAgent { - id: string; + id: string; } export interface IReceiveInvite { - alias?: string; - label?: string; - imageUrl?: string; - autoAcceptConnection?: boolean; - autoAcceptInvitation?: boolean; - reuseConnection?: boolean; - acceptInvitationTimeoutMs?: number; + alias?: string; + label?: string; + imageUrl?: string; + autoAcceptConnection?: boolean; + autoAcceptInvitation?: boolean; + reuseConnection?: boolean; + acceptInvitationTimeoutMs?: number; } export interface IReceiveInvitationUrl extends IReceiveInvite { - invitationUrl: string; + invitationUrl: string; } interface IService { - id: string; - serviceEndpoint: string; - type: string; - recipientKeys: string[]; - routingKeys: string[]; - accept: string[]; + id: string; + serviceEndpoint: string; + type: string; + recipientKeys: string[]; + routingKeys: string[]; + accept: string[]; } interface IInvitation { - '@id': string; - '@type': string; - label: string; - goalCode: string; - goal: string; - accept: string[]; - handshake_protocols: string[]; - services: (IService | string)[]; - imageUrl?: string; + '@id': string; + '@type': string; + label: string; + goalCode: string; + goal: string; + accept: string[]; + handshake_protocols: string[]; + services: (IService | string)[]; + imageUrl?: string; } export interface IReceiveInvitation extends IReceiveInvite { - invitation: IInvitation; + invitation: IInvitation; } export interface IProofPresentation { - createdAt: string; - protocolVersion: string; - state: string; - connectionId: string; - threadId: string; - autoAcceptProof: string; - updatedAt: string; - isVerified: boolean; + createdAt: string; + protocolVersion: string; + state: string; + connectionId: string; + threadId: string; + autoAcceptProof: string; + updatedAt: string; + isVerified: boolean; } interface ITags { - connectionId: string; - state: string; - threadId: string; + connectionId: string; + state: string; + threadId: string; +} + +export interface IValidResponses { + text: string; +} +export interface IQuestionPayload { + detail: string; + validResponses: IValidResponses[]; + question: string; + orgId?: string; + connectionId: string; + tenantId: string; +} +interface Ledger { + id: string; + createDateTime: string; + lastChangedDateTime: string; + name: string; + networkType: string; + poolConfig: string; + isActive: boolean; + networkString: string; + registerDIDEndpoint: string; + registerDIDPayload: string; + indyNamespace: string; + networkUrl: string | null; +} + +export interface LedgerListResponse { + response: Ledger[]; +} + +export interface ICreateConnectionInvitation { + label?: string; + alias?: string; + imageUrl?: string; + goalCode?: string; + goal?: string; + handshake?: boolean; + handshakeProtocols?: object[]; + messages?: object[]; + multiUseInvitation?: boolean; + autoAcceptConnection?: boolean; + routing?: object; + appendedAttachments?: object[]; + orgId?: string; } - diff --git a/apps/agent-service/src/main.ts b/apps/agent-service/src/main.ts index 15e462947..efafbfbc4 100644 --- a/apps/agent-service/src/main.ts +++ b/apps/agent-service/src/main.ts @@ -32,8 +32,12 @@ async function bootstrap(): Promise { orgName: `${CommonConstants.PLATFORM_ADMIN_ORG}`, platformAdminEmail: process.env.PLATFORM_ADMIN_EMAIL, tenant: true, - ledgerName: [Ledgers.Bcovrin_Testnet, Ledgers.Indicio_Demonet, Ledgers.Indicio_Mainnet, Ledgers.Indicio_Testnet] - }; + ledgerName: [Ledgers.Bcovrin_Testnet, Ledgers.Indicio_Demonet, Ledgers.Indicio_Mainnet, Ledgers.Indicio_Testnet], + keyType: `${CommonConstants.KEYTYPE}`, + method: `${CommonConstants.METHOD}`, + network: `${CommonConstants.NETWORK}`, + role: `${CommonConstants.ROLE}` +}; const agentService = app.get(AgentServiceService); await agentService.walletProvision(agentSpinupPayload, user); diff --git a/apps/agent-service/src/repositories/agent-service.repository.ts b/apps/agent-service/src/repositories/agent-service.repository.ts index 78133e7a2..203941378 100644 --- a/apps/agent-service/src/repositories/agent-service.repository.ts +++ b/apps/agent-service/src/repositories/agent-service.repository.ts @@ -1,8 +1,8 @@ import { PrismaService } from '@credebl/prisma-service'; import { Injectable, Logger } from '@nestjs/common'; // eslint-disable-next-line camelcase -import { ledgers, org_agents, organisation, platform_config, user } from '@prisma/client'; -import { ICreateOrgAgent, IStoreOrgAgentDetails, IOrgAgent, IOrgAgentsResponse, IOrgLedgers, IStoreAgent } from '../interface/agent-service.interface'; +import { ledgerConfig, ledgers, org_agents, org_agents_type, organisation, platform_config, user } from '@prisma/client'; +import { ICreateOrgAgent, IOrgAgent, IOrgAgentsResponse, IOrgLedgers, IStoreAgent, IStoreOrgAgentDetails } from '../interface/agent-service.interface'; import { AgentType } from '@credebl/enum/enum'; @Injectable() @@ -26,6 +26,15 @@ export class AgentServiceRepository { } } + async getLedgerConfigByOrgId(): Promise { + try { + const ledgerConfigData = await this.prisma.ledgerConfig.findMany(); + return ledgerConfigData; + } catch (error) { + this.logger.error(`[getGenesisUrl] - get genesis URL: ${JSON.stringify(error)}`); + throw error; + } + } /** * Get genesis url * @param id @@ -107,7 +116,7 @@ export class AgentServiceRepository { } - /** + /** * Store agent details * @param storeAgentDetails * @returns @@ -116,12 +125,13 @@ export class AgentServiceRepository { async storeOrgAgentDetails(storeOrgAgentDetails: IStoreOrgAgentDetails): Promise { try { - return this.prisma.org_agents.update({ + return await this.prisma.org_agents.update({ where: { id: storeOrgAgentDetails.id }, data: { orgDid: storeOrgAgentDetails.did, + didDocument: storeOrgAgentDetails.didDoc, verkey: storeOrgAgentDetails.verkey, isDidPublic: storeOrgAgentDetails.isDidPublic, agentSpinUpStatus: storeOrgAgentDetails.agentSpinUpStatus, @@ -313,6 +323,23 @@ export class AgentServiceRepository { } } + // eslint-disable-next-line camelcase + async getOrgAgentType(orgAgentId: string): Promise { + try { + const orgAgent = await this.prisma.org_agents_type.findUnique({ + where: { + id: orgAgentId + } + }); + + return orgAgent; + + } catch (error) { + this.logger.error(`[getOrgAgentType] - error: ${JSON.stringify(error)}`); + throw error; + } + } + async getPlatfomAdminUser(platformAdminUserEmail: string): Promise { try { const platformAdminUser = await this.prisma.user.findUnique({ @@ -344,4 +371,4 @@ export class AgentServiceRepository { throw error; } } -} +} \ No newline at end of file diff --git a/apps/api-gateway/src/agent-service/agent-service.controller.ts b/apps/api-gateway/src/agent-service/agent-service.controller.ts index 52c39c16e..c69c4d75b 100644 --- a/apps/api-gateway/src/agent-service/agent-service.controller.ts +++ b/apps/api-gateway/src/agent-service/agent-service.controller.ts @@ -32,8 +32,12 @@ import { CustomExceptionFilter } from 'apps/api-gateway/common/exception-handler import { Roles } from '../authz/decorators/roles.decorator'; import { OrgRoles } from 'libs/org-roles/enums'; import { OrgRolesGuard } from '../authz/guards/org-roles.guard'; +import { CreateDidDto } from './dto/create-did.dto'; +import { validateDid } from '@credebl/common/did.validator'; +import { CreateWalletDto } from './dto/create-wallet.dto'; const seedLength = 32; + @UseFilters(CustomExceptionFilter) @Controller() @ApiTags('agents') @@ -75,6 +79,29 @@ export class AgentController { } + @Get('/orgs/agents/ledgerConfig') + @ApiOperation({ + summary: 'Get the ledger config details', + description: 'Get the ledger config details' + }) + @UseGuards(AuthGuard('jwt')) + async getLedgerDetails( + @User() reqUser: user, + @Res() res: Response + ): Promise { + + const ledgerConfigData = await this.agentService.getLedgerConfig(reqUser); + + const finalResponse: IResponse = { + statusCode: HttpStatus.OK, + message: ResponseMessages.agent.success.ledgerConfig, + data: ledgerConfigData + }; + + return res.status(HttpStatus.OK).json(finalResponse); + + } + /** * Spinup the agent by organization * @param agentSpinupDto @@ -96,14 +123,6 @@ export class AgentController { @Res() res: Response ): Promise { - if (seedLength !== agentSpinupDto.seed.length) { - this.logger.error(`seed must be at most 32 characters.`); - throw new BadRequestException( - ResponseMessages.agent.error.seedChar, - { cause: new Error(), description: ResponseMessages.errorMessages.badRequest } - ); - } - const regex = new RegExp('^[a-zA-Z0-9]+$'); if (!regex.test(agentSpinupDto.walletName)) { @@ -154,14 +173,6 @@ export class AgentController { createTenantDto.orgId = orgId; - if (seedLength !== createTenantDto.seed.length) { - this.logger.error(`seed must be at most 32 characters`); - throw new BadRequestException( - ResponseMessages.agent.error.seedCharCount, - { cause: new Error(), description: ResponseMessages.errorMessages.badRequest } - ); - } - const tenantDetails = await this.agentService.createTenant(createTenantDto, user); const finalResponse: IResponse = { @@ -172,4 +183,103 @@ export class AgentController { return res.status(HttpStatus.CREATED).json(finalResponse); } + + /** + * Create wallet + * @param orgId + * @returns wallet + */ + @Post('/orgs/:orgId/agents/createWallet') + @ApiOperation({ + summary: 'Create wallet', + description: 'Create wallet' + }) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + @Roles(OrgRoles.OWNER, OrgRoles.ADMIN) + @ApiResponse({ status: HttpStatus.CREATED, description: 'Success', type: ApiResponseDto }) + async createWallet( + @Param('orgId') orgId: string, + @Body() createWalletDto: CreateWalletDto, + @User() user: user, + @Res() res: Response + ): Promise { + + createWalletDto.orgId = orgId; + const walletDetails = await this.agentService.createWallet(createWalletDto, user); + + const finalResponse: IResponse = { + statusCode: HttpStatus.CREATED, + message: ResponseMessages.agent.success.createWallet, + data: walletDetails + }; + + return res.status(HttpStatus.CREATED).json(finalResponse); + } + + // This function will be used after multiple did method implementation in create wallet + /** + * Create did + * @param orgId + * @returns did + */ + @Post('/orgs/:orgId/agents/createDid') + @ApiOperation({ + summary: 'Create did', + description: 'Create did' + }) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + @Roles(OrgRoles.OWNER, OrgRoles.ADMIN) + @ApiResponse({ status: HttpStatus.CREATED, description: 'Success', type: ApiResponseDto }) + async createDid( + @Param('orgId') orgId: string, + @Body() createDidDto: CreateDidDto, + @User() user: user, + @Res() res: Response + ): Promise { + + await validateDid(createDidDto); + + if (createDidDto.seed && seedLength !== createDidDto.seed.length) { + this.logger.error(`seed must be at most 32 characters.`); + throw new BadRequestException( + ResponseMessages.agent.error.seedChar, + { cause: new Error(), description: ResponseMessages.errorMessages.badRequest } + ); + } + + const didDetails = await this.agentService.createDid(createDidDto, orgId, user); + + const finalResponse: IResponse = { + statusCode: HttpStatus.CREATED, + message: ResponseMessages.agent.success.createDid, + data: didDetails + }; + + return res.status(HttpStatus.CREATED).json(finalResponse); + } + + /** + * Create Secp256k1 key pair for polygon DID + * @param orgId + * @returns Secp256k1 key pair for polygon DID + */ + @Post('/orgs/:orgId/agents/polygon/create-keys') + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + @Roles(OrgRoles.OWNER, OrgRoles.ADMIN, OrgRoles.PLATFORM_ADMIN, OrgRoles.ISSUER, OrgRoles.VERIFIER) + @ApiResponse({ status: HttpStatus.CREATED, description: 'Success', type: ApiResponseDto }) + async createSecp256k1KeyPair( + @Param('orgId') orgId: string, + @Res() res: Response + ): Promise { + + const didDetails = await this.agentService.createSecp256k1KeyPair(orgId); + + const finalResponse: IResponse = { + statusCode: HttpStatus.CREATED, + message: ResponseMessages.agent.success.createKeys, + data: didDetails + }; + + return res.status(HttpStatus.CREATED).json(finalResponse); + } } \ No newline at end of file diff --git a/apps/api-gateway/src/agent-service/agent-service.service.ts b/apps/api-gateway/src/agent-service/agent-service.service.ts index 417512bed..48cead6c9 100644 --- a/apps/api-gateway/src/agent-service/agent-service.service.ts +++ b/apps/api-gateway/src/agent-service/agent-service.service.ts @@ -4,8 +4,10 @@ import { user } from '@prisma/client'; import { BaseService } from 'libs/service/base.service'; import { AgentSpinupDto } from './dto/agent-service.dto'; import { CreateTenantDto } from './dto/create-tenant.dto'; -import { AgentSpinUpSatus } from './interface/agent-service.interface'; +import { AgentSpinUpSatus, IWalletRecord } from './interface/agent-service.interface'; import { AgentStatus } from './interface/agent-service.interface'; +import { CreateDidDto } from './dto/create-did.dto'; +import { CreateWalletDto } from './dto/create-wallet.dto'; @Injectable() export class AgentService extends BaseService { @@ -35,6 +37,19 @@ export class AgentService extends BaseService { return this.sendNatsMessage(this.agentServiceProxy, 'create-tenant', payload); } + async createDid(createDidDto: CreateDidDto, orgId:string, user: user): Promise { + const payload = { createDidDto, orgId, user }; + + // NATS call + return this.sendNatsMessage(this.agentServiceProxy, 'create-did', payload); + } + + async createWallet(createWalletDto: CreateWalletDto, user: user): Promise { + const payload = { createWalletDto, user }; + // NATS call + return this.sendNatsMessage(this.agentServiceProxy, 'create-wallet', payload); + } + async getAgentHealth(user: user, orgId:string): Promise { const payload = { user, orgId }; @@ -43,4 +58,18 @@ export class AgentService extends BaseService { } -} + async getLedgerConfig(user: user): Promise { + const payload = { user }; + + // NATS call + return this.sendNatsMessage(this.agentServiceProxy, 'get-ledger-config', payload); + } + + async createSecp256k1KeyPair(orgId:string): Promise { + const payload = {orgId}; + // NATS call + + return this.sendNatsMessage(this.agentServiceProxy, 'polygon-create-keys', payload); + } + +} \ No newline at end of file diff --git a/apps/api-gateway/src/agent-service/dto/agent-service.dto.ts b/apps/api-gateway/src/agent-service/dto/agent-service.dto.ts index a668a0025..b0cc851d2 100644 --- a/apps/api-gateway/src/agent-service/dto/agent-service.dto.ts +++ b/apps/api-gateway/src/agent-service/dto/agent-service.dto.ts @@ -1,19 +1,19 @@ import { trim } from '@credebl/common/cast.helper'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { Transform } from 'class-transformer'; -import { IsBoolean, IsNotEmpty, IsOptional, IsString, Matches, MaxLength, MinLength, IsArray } from 'class-validator'; +import { IsBoolean, IsNotEmpty, IsOptional, IsString, Matches, MaxLength, MinLength } from 'class-validator'; +import { CreateDidDto } from './create-did.dto'; const regex = /^[a-zA-Z0-9 ]*$/; -export class AgentSpinupDto { +export class AgentSpinupDto extends CreateDidDto { @ApiProperty() + @MaxLength(25, { message: 'Maximum length for wallet must be 25 characters.' }) + @IsString({ message: 'label must be in string format.' }) @Transform(({ value }) => trim(value)) - @IsNotEmpty({ message: 'walletName is required' }) - @MinLength(2, { message: 'walletName must be at least 2 characters.' }) - @MaxLength(50, { message: 'walletName must be at most 50 characters.' }) - @IsString({ message: 'walletName must be in string format.' }) + @MinLength(2, { message: 'Minimum length for wallet name must be 2 characters.' }) @Matches(regex, { message: 'Wallet name must not contain special characters.' }) @Matches(/^\S*$/, { - message: 'Spaces are not allowed in wallet name' + message: 'Spaces are not allowed in label' }) walletName: string; @@ -23,31 +23,12 @@ export class AgentSpinupDto { @IsNotEmpty({ message: 'Password is required.' }) walletPassword: string; - @ApiProperty({ example: 'dfuhgfklskmjngrjekjfgjjfkoekfdad' }) - @Transform(({ value }) => trim(value)) - @IsNotEmpty({ message: 'seed is required' }) - @MaxLength(32, { message: 'seed must be at most 32 characters.' }) - @IsString({ message: 'seed must be in string format.' }) - @Matches(/^\S*$/, { - message: 'Spaces are not allowed in seed' - }) - seed: string; - @ApiProperty({ example: 'XzFjo1RTZ2h9UVFCnPUyaQ' }) @IsOptional() @ApiPropertyOptional() @IsString({ message: 'did must be in string format.' }) did?: string; - @ApiProperty({ example: ['6ba7b810-9dad-11d1-80b4-00c04fd430c8'] }) - @IsOptional() - @ApiPropertyOptional() - @IsArray({ message: 'ledgerId must be an array' }) - @IsString({ each: true, message: 'Each ledgerId must be a string' }) - @MaxLength(36, { each: true, message: 'ledgerId must be at most 36 characters.' }) - @IsNotEmpty({ message: 'please provide valid ledgerId' }) - ledgerId?: string[]; - @ApiProperty({ example: 'ojIckSD2jqNzOqIrAGzL' }) @IsOptional() @ApiPropertyOptional() diff --git a/apps/api-gateway/src/agent-service/dto/create-did.dto.ts b/apps/api-gateway/src/agent-service/dto/create-did.dto.ts new file mode 100644 index 000000000..bc36bac98 --- /dev/null +++ b/apps/api-gateway/src/agent-service/dto/create-did.dto.ts @@ -0,0 +1,78 @@ +import { trim } from '@credebl/common/cast.helper'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { Transform } from 'class-transformer'; +import { MaxLength, IsString, Matches, IsNotEmpty, IsOptional } from 'class-validator'; + +export class CreateDidDto { + + @ApiProperty({ example: '000000000000000000000000000Seed1' }) + @MaxLength(32, { message: 'seed must be at most 32 characters.' }) + @Transform(({ value }) => trim(value)) + @IsOptional() + @ApiPropertyOptional() + @IsString({ message: 'seed must be in string format.' }) + @Matches(/^\S*$/, { + message: 'Spaces are not allowed in seed' + }) + seed?: string; + + @ApiProperty({ example: 'ed25519'}) + @IsNotEmpty({ message: 'key type is required' }) + @Transform(({ value }) => trim(value)) + @IsString({ message: 'key type be in string format.' }) + keyType: string; + + @ApiProperty({ example: 'indy'}) + @IsNotEmpty({ message: 'method is required' }) + @Transform(({ value }) => trim(value)) + @IsString({ message: 'method must be in string format.' }) + method: string; + + @ApiProperty({example: 'bcovrin:testnet'}) + @IsOptional() + @ApiPropertyOptional() + @Transform(({ value }) => trim(value)) + @IsString({ message: 'network must be in string format.' }) + network?: string; + + @ApiProperty({example: 'www.github.com'}) + @IsOptional() + @ApiPropertyOptional() + @Transform(({ value }) => trim(value)) + @IsString({ message: 'domain must be in string format.' }) + domain?: string; + + @ApiProperty({example: 'endorser'}) + @IsOptional() + @ApiPropertyOptional() + @Transform(({ value }) => trim(value)) + @IsString({ message: 'role must be in string format.' }) + role?: string; + + @ApiProperty({example: ''}) + @IsOptional() + @ApiPropertyOptional() + @IsString({ message: 'private key must be in string format.' }) + @Transform(({ value }) => trim(value)) + privatekey?: string; + + @ApiProperty({example: 'http://localhost:6006/docs'}) + @IsOptional() + @ApiPropertyOptional() + @IsString({ message: 'endpoint must be in string format.' }) + endpoint?: string; + + @ApiProperty({ example: 'XzFjo1RTZ2h9UVFCnPUyaQ' }) + @IsOptional() + @ApiPropertyOptional() + @Transform(({ value }) => trim(value)) + @IsString({ message: 'did must be in string format.' }) + did?: string; + + @ApiProperty({example: 'did:indy:bcovrin:testnet:UEeW111G1tYo1nEkPwMcF'}) + @IsOptional() + @ApiPropertyOptional() + @Transform(({ value }) => trim(value)) + @IsString({ message: 'endorser did must be in string format.' }) + endorserDid?: string; +} \ No newline at end of file diff --git a/apps/api-gateway/src/agent-service/dto/create-tenant.dto.ts b/apps/api-gateway/src/agent-service/dto/create-tenant.dto.ts index 9204d6370..e26131431 100644 --- a/apps/api-gateway/src/agent-service/dto/create-tenant.dto.ts +++ b/apps/api-gateway/src/agent-service/dto/create-tenant.dto.ts @@ -1,48 +1,28 @@ import { trim } from '@credebl/common/cast.helper'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { Transform } from 'class-transformer'; -import { MaxLength, IsString, MinLength, Matches, IsNotEmpty, IsOptional, IsArray } from 'class-validator'; -const labelRegex = /^[a-zA-Z0-9 ]*$/; -export class CreateTenantDto { +import { MaxLength, IsString, MinLength, IsOptional } from 'class-validator'; +import { CreateDidDto } from './create-did.dto'; +export class CreateTenantDto extends CreateDidDto { @ApiProperty() @MaxLength(25, { message: 'Maximum length for label must be 25 characters.' }) @IsString({ message: 'label must be in string format.' }) @Transform(({ value }) => trim(value)) @MinLength(2, { message: 'Minimum length for label must be 2 characters.' }) - @Matches(labelRegex, { message: 'Label must not contain special characters.' }) - @Matches(/^\S*$/, { - message: 'Spaces are not allowed in label' - }) label: string; - @ApiProperty({ example: 'dfuhgfklskmjngrjekjfgjjfkoekfdad' }) - @MaxLength(32, { message: 'seed must be at most 32 characters.' }) - @Transform(({ value }) => trim(value)) - @IsNotEmpty({ message: 'seed is required' }) - @IsString({ message: 'seed must be in string format.' }) - @Matches(/^\S*$/, { - message: 'Spaces are not allowed in seed' - }) - seed: string; - - @ApiProperty({ example: [1] }) - @ApiPropertyOptional() - @IsOptional() - @IsArray({ message: 'ledgerId must be an array' }) - @IsNotEmpty({ message: 'please provide valid ledgerId' }) - ledgerId?: string[]; - - @ApiProperty({ example: 'XzFjo1RTZ2h9UVFCnPUyaQ' }) + @ApiProperty({ example: 'ojIckSD2jqNzOqIrAGzL' }) @IsOptional() @ApiPropertyOptional() @IsString({ message: 'did must be in string format.' }) - did?: string; + clientSocketId?: string; - @ApiProperty({ example: 'ojIckSD2jqNzOqIrAGzL' }) + @ApiProperty({ example: 'XzFjo1RTZ2h9UVFCnPUyaQ' }) @IsOptional() @ApiPropertyOptional() @IsString({ message: 'did must be in string format.' }) - clientSocketId?: string; + did?: string; orgId: string; + } \ No newline at end of file diff --git a/apps/api-gateway/src/agent-service/dto/create-wallet.dto.ts b/apps/api-gateway/src/agent-service/dto/create-wallet.dto.ts new file mode 100644 index 000000000..eec4ac54c --- /dev/null +++ b/apps/api-gateway/src/agent-service/dto/create-wallet.dto.ts @@ -0,0 +1,28 @@ +import { trim } from '@credebl/common/cast.helper'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { Transform } from 'class-transformer'; +import { MaxLength, IsString, MinLength, IsNotEmpty, IsOptional } from 'class-validator'; + +export class CreateWalletDto { + @ApiProperty() + @MaxLength(25, { message: 'Maximum length for label must be 25 characters.' }) + @IsString({ message: 'label must be in string format.' }) + @Transform(({ value }) => trim(value)) + @IsNotEmpty({ message: 'label is required' }) + @MinLength(2, { message: 'Minimum length for label must be 2 characters.' }) + label: string; + + @ApiProperty({ example: 'XzFjo1RTZ2h9UVFCnPUyaQ' }) + @IsOptional() + @ApiPropertyOptional() + @IsString({ message: 'did must be in string format.' }) + did?: string; + + @ApiProperty({ example: 'ojIckSD2jqNzOqIrAGzL' }) + @IsOptional() + @ApiPropertyOptional() + @IsString({ message: 'did must be in string format.' }) + clientSocketId?: string; + + orgId: string; +} \ No newline at end of file diff --git a/apps/api-gateway/src/agent-service/interface/agent-service.interface.ts b/apps/api-gateway/src/agent-service/interface/agent-service.interface.ts index 6ced1ed42..25511321e 100644 --- a/apps/api-gateway/src/agent-service/interface/agent-service.interface.ts +++ b/apps/api-gateway/src/agent-service/interface/agent-service.interface.ts @@ -5,4 +5,22 @@ export interface AgentStatus { label: string; endpoints: string[]; isInitialized: boolean; +} +interface IWalletConfig { + id: string; + key: string; + keyDerivationMethod: string; +} + +interface IConfig { + label: string; + walletConfig: IWalletConfig; +} +export interface IWalletRecord { + _tags: string; + metadata: string; + id: string; + createdAt: string; + config: IConfig; + updatedAt: string; } \ No newline at end of file diff --git a/apps/api-gateway/src/app.module.ts b/apps/api-gateway/src/app.module.ts index b4b297e94..8b853a00e 100644 --- a/apps/api-gateway/src/app.module.ts +++ b/apps/api-gateway/src/app.module.ts @@ -26,6 +26,7 @@ import { CacheModule } from '@nestjs/cache-manager'; import * as redisStore from 'cache-manager-redis-store'; import { WebhookModule } from './webhook/webhook.module'; import { UtilitiesModule } from './utilities/utilities.module'; +import { NotificationModule } from './notification/notification.module'; @Module({ imports: [ @@ -52,6 +53,7 @@ import { UtilitiesModule } from './utilities/utilities.module'; EcosystemModule, UtilitiesModule, WebhookModule, + NotificationModule, CacheModule.register({ store: redisStore, host: process.env.REDIS_HOST, port: process.env.REDIS_PORT }), BullModule.forRoot({ redis: { diff --git a/apps/api-gateway/src/authz/authz.controller.ts b/apps/api-gateway/src/authz/authz.controller.ts index 36d2fda25..f93d61af4 100644 --- a/apps/api-gateway/src/authz/authz.controller.ts +++ b/apps/api-gateway/src/authz/authz.controller.ts @@ -27,6 +27,7 @@ import { CustomExceptionFilter } from 'apps/api-gateway/common/exception-handler import { ResetPasswordDto } from './dtos/reset-password.dto'; import { ForgotPasswordDto } from './dtos/forgot-password.dto'; import { ResetTokenPasswordDto } from './dtos/reset-token-password'; +import { RefreshTokenDto } from './dtos/refresh-token.dto'; @Controller('auth') @@ -176,5 +177,24 @@ export class AuthzController { } + @Post('/refresh-token') + @ApiOperation({ + summary: 'Token from refresh token', + description: 'Get a new token from a refresh token' + }) + @ApiResponse({ status: HttpStatus.OK, description: 'Success', type: ApiResponseDto }) + async refreshToken( + @Body() refreshTokenDto: RefreshTokenDto, + @Res() res: Response): Promise { + const tokenData = await this.authzService.refreshToken(refreshTokenDto.refreshToken); + const finalResponse: IResponseType = { + statusCode: HttpStatus.OK, + message: ResponseMessages.user.success.refreshToken, + data: tokenData + }; + + return res.status(HttpStatus.OK).json(finalResponse); + + } } \ No newline at end of file diff --git a/apps/api-gateway/src/authz/authz.service.ts b/apps/api-gateway/src/authz/authz.service.ts index 8cb6b7d01..7c58d0f7a 100644 --- a/apps/api-gateway/src/authz/authz.service.ts +++ b/apps/api-gateway/src/authz/authz.service.ts @@ -58,6 +58,10 @@ export class AuthzService extends BaseService { return this.sendNatsMessage(this.authServiceProxy, 'user-set-token-password', resetTokenPasswordDto); } + async refreshToken(refreshToken: string): Promise { + return this.sendNatsMessage(this.authServiceProxy, 'refresh-token-details', refreshToken); + } + async addUserDetails(userInfo: AddUserDetailsDto): Promise { const payload = { userInfo }; return this.sendNatsMessage(this.authServiceProxy, 'add-user', payload); diff --git a/apps/api-gateway/src/authz/dtos/refresh-token.dto.ts b/apps/api-gateway/src/authz/dtos/refresh-token.dto.ts new file mode 100644 index 000000000..e9ad0f6fa --- /dev/null +++ b/apps/api-gateway/src/authz/dtos/refresh-token.dto.ts @@ -0,0 +1,14 @@ +import { IsNotEmpty } from 'class-validator'; + +import { ApiProperty } from '@nestjs/swagger'; +import { Transform } from 'class-transformer'; +import { trim } from '@credebl/common/cast.helper'; + +export class RefreshTokenDto { + + @ApiProperty() + @Transform(({ value }) => trim(value)) + @IsNotEmpty({ message: 'refreshToken is required.' }) + refreshToken: string; + +} \ No newline at end of file diff --git a/apps/api-gateway/src/authz/guards/ecosystem-roles.guard.ts b/apps/api-gateway/src/authz/guards/ecosystem-roles.guard.ts index a0f5542fc..af5b5b619 100644 --- a/apps/api-gateway/src/authz/guards/ecosystem-roles.guard.ts +++ b/apps/api-gateway/src/authz/guards/ecosystem-roles.guard.ts @@ -1,8 +1,4 @@ -import { BadRequestException, CanActivate, ExecutionContext, Logger } from '@nestjs/common'; - -import { HttpException } from '@nestjs/common'; -import { HttpStatus } from '@nestjs/common'; -import { Injectable } from '@nestjs/common'; +import { BadRequestException, CanActivate, ExecutionContext, ForbiddenException, Logger, Injectable } from '@nestjs/common'; import { ECOSYSTEM_ROLES_KEY } from '../decorators/roles.decorator'; import { Reflector } from '@nestjs/core'; import { EcosystemService } from '../../ecosystem/ecosystem.service'; @@ -54,22 +50,25 @@ export class EcosystemRolesGuard implements CanActivate { const ecosystemOrgData = await this.ecosystemService.fetchEcosystemOrg(ecosystemId, orgId); if (!ecosystemOrgData) { - throw new HttpException('Organization does not match', HttpStatus.FORBIDDEN); + throw new ForbiddenException(ResponseMessages.organisation.error.orgDoesNotMatch); } - const {response} = ecosystemOrgData; - - user.ecosystemOrgRole = response['ecosystemRole']['name']; + user.ecosystemOrgRole = ecosystemOrgData['ecosystemRole']['name']; if (!user.ecosystemOrgRole) { - throw new HttpException('Ecosystem role not match', HttpStatus.FORBIDDEN); + throw new ForbiddenException(ResponseMessages.ecosystem.error.ecosystemRoleNotMatch); } } else { - throw new HttpException('organization & ecosystem is required', HttpStatus.BAD_REQUEST); + throw new BadRequestException(ResponseMessages.ecosystem.error.orgEcoIdRequired); + } + + // Sending user friendly message if a user attempts to access an API that is inaccessible to their role + const roleAccess = requiredRoles.some((role) => user.ecosystemOrgRole === role); + if (!roleAccess) { + throw new ForbiddenException(ResponseMessages.organisation.error.roleNotMatch, { cause: new Error(), description: ResponseMessages.errorMessages.forbidden }); } - return requiredRoles.some((role) => user.ecosystemOrgRole === role); - + return roleAccess; } } \ No newline at end of file diff --git a/apps/api-gateway/src/authz/guards/org-roles.guard.ts b/apps/api-gateway/src/authz/guards/org-roles.guard.ts index b4e71fb26..f3c1178d1 100644 --- a/apps/api-gateway/src/authz/guards/org-roles.guard.ts +++ b/apps/api-gateway/src/authz/guards/org-roles.guard.ts @@ -1,7 +1,5 @@ import { BadRequestException, CanActivate, ExecutionContext, ForbiddenException, Logger } from '@nestjs/common'; -import { HttpException } from '@nestjs/common'; -import { HttpStatus } from '@nestjs/common'; import { Injectable } from '@nestjs/common'; import { OrgRoles } from 'libs/org-roles/enums'; import { ROLES_KEY } from '../decorators/roles.decorator'; @@ -34,15 +32,23 @@ export class OrgRolesGuard implements CanActivate { const orgId = req.params.orgId || req.query.orgId || req.body.orgId; - if (!orgId) { - throw new BadRequestException(ResponseMessages.organisation.error.orgIdIsRequired); - } + if (orgId) { if (!isValidUUID(orgId)) { throw new BadRequestException(ResponseMessages.organisation.error.invalidOrgId); - } - - if (orgId) { + } + + + if (user.hasOwnProperty('resource_access') && user.resource_access[orgId]) { + const orgRoles: string[] = user.resource_access[orgId].roles; + const roleAccess = requiredRoles.some((role) => orgRoles.includes(role)); + + if (!roleAccess) { + throw new ForbiddenException(ResponseMessages.organisation.error.roleNotMatch, { cause: new Error(), description: ResponseMessages.errorMessages.forbidden }); + } + return roleAccess; + } + const specificOrg = user.userOrgRoles.find((orgDetails) => { if (!orgDetails.orgId) { return false; @@ -78,7 +84,7 @@ export class OrgRolesGuard implements CanActivate { return false; } else { - throw new HttpException('organization is required', HttpStatus.BAD_REQUEST); + throw new BadRequestException('Please provide valid orgId'); } // Sending user friendly message if a user attempts to access an API that is inaccessible to their role diff --git a/apps/api-gateway/src/authz/guards/user-access-guard.ts b/apps/api-gateway/src/authz/guards/user-access-guard.ts new file mode 100644 index 000000000..ea53f63fc --- /dev/null +++ b/apps/api-gateway/src/authz/guards/user-access-guard.ts @@ -0,0 +1,16 @@ +import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common'; +import { Observable } from 'rxjs'; + +@Injectable() +export class UserAccessGuard implements CanActivate { + canActivate(context: ExecutionContext): boolean | Promise | Observable { + const request = context.switchToHttp().getRequest(); + + const { user } = request; + + if (user.hasOwnProperty('client_id')) { + throw new UnauthorizedException('You do not have access'); + } + return true; + } +} diff --git a/apps/api-gateway/src/connection/connection.controller.ts b/apps/api-gateway/src/connection/connection.controller.ts index 6ca88310d..d3dad9185 100644 --- a/apps/api-gateway/src/connection/connection.controller.ts +++ b/apps/api-gateway/src/connection/connection.controller.ts @@ -7,7 +7,7 @@ import { User } from '../authz/decorators/user.decorator'; import { ForbiddenErrorDto } from '../dtos/forbidden-error.dto'; import { UnauthorizedErrorDto } from '../dtos/unauthorized-error.dto'; import { ConnectionService } from './connection.service'; -import { ConnectionDto, CreateConnectionDto, ReceiveInvitationDto, ReceiveInvitationUrlDto } from './dtos/connection.dto'; +import { ConnectionDto, CreateConnectionDto, CreateOutOfBandConnectionInvitation, ReceiveInvitationDto, ReceiveInvitationUrlDto } from './dtos/connection.dto'; import { IUserRequestInterface } from './interfaces'; import { Response } from 'express'; import { IUserRequest } from '@credebl/user-request/user-request.interface'; @@ -15,11 +15,12 @@ import { CustomExceptionFilter } from 'apps/api-gateway/common/exception-handler import { OrgRoles } from 'libs/org-roles/enums'; import { Roles } from '../authz/decorators/roles.decorator'; import { OrgRolesGuard } from '../authz/guards/org-roles.guard'; -import { GetAllConnectionsDto } from './dtos/get-all-connections.dto'; +import { GetAllAgentConnectionsDto, GetAllConnectionsDto } from './dtos/get-all-connections.dto'; import { ApiResponseDto } from '../dtos/apiResponse.dto'; import { IConnectionSearchCriteria } from '../interfaces/IConnectionSearch.interface'; import { SortFields } from 'apps/connection/src/enum/connection.enum'; import { ClientProxy} from '@nestjs/microservices'; +import { QuestionAnswerWebhookDto, QuestionDto} from './dtos/question-answer.dto'; @UseFilters(CustomExceptionFilter) @Controller() @@ -107,6 +108,61 @@ export class ConnectionController { return res.status(HttpStatus.OK).json(finalResponse); } + /** + * Description: Get all connections from agent + * @param user + * @param orgId + * + */ + @Get('/orgs/:orgId/agent/connections') + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + @Roles(OrgRoles.OWNER, OrgRoles.ADMIN, OrgRoles.ISSUER, OrgRoles.VERIFIER, OrgRoles.MEMBER) + @ApiOperation({ + summary: `Fetch all connections from agent by orgId`, + description: `Fetch all connections from agent by orgId` + }) + @ApiResponse({ status: HttpStatus.OK, description: 'Success', type: ApiResponseDto }) + async getConnectionListFromAgent( + @Query() getAllConnectionsDto: GetAllAgentConnectionsDto, + @Param('orgId') orgId: string, + @Res() res: Response + ): Promise { + + const connectionDetails = await this.connectionService.getConnectionListFromAgent( + getAllConnectionsDto, + orgId + ); + + const finalResponse: IResponse = { + statusCode: HttpStatus.OK, + message: ResponseMessages.connection.success.fetch, + data: connectionDetails + }; + return res.status(HttpStatus.OK).json(finalResponse); + } + + + @Get('orgs/:orgId/question-answer/question') + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + @Roles(OrgRoles.OWNER, OrgRoles.ADMIN, OrgRoles.ISSUER, OrgRoles.VERIFIER, OrgRoles.MEMBER, OrgRoles.HOLDER, OrgRoles.SUPER_ADMIN, OrgRoles.PLATFORM_ADMIN) + @ApiOperation({ + summary: `Get question-answer record`, + description: `Get question-answer record` + }) + @ApiResponse({ status: HttpStatus.OK, description: 'Success', type: ApiResponseDto }) + async getQuestionAnswersRecord( + @Param('orgId') orgId: string, + @Res() res: Response + ): Promise { + const record = await this.connectionService.getQuestionAnswersRecord(orgId); + const finalResponse: IResponse = { + statusCode: HttpStatus.OK, + message: ResponseMessages.connection.success.questionAnswerRecord, + data: record + }; + return res.status(HttpStatus.OK).json(finalResponse); + } + /** * Create out-of-band connection legacy invitation * @param connectionDto @@ -134,6 +190,60 @@ export class ConnectionController { }; return res.status(HttpStatus.CREATED).json(finalResponse); + } + + /** + * Create out-of-band connection invitation + * @param connectionDto + * @param res + * @returns Created out-of-band connection invitation url + */ + @Post('/orgs/:orgId/connection-invitation') + @ApiOperation({ summary: 'Create outbound out-of-band connection invitation', description: 'Create outbound out-of-band connection invitation' }) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + @Roles(OrgRoles.OWNER, OrgRoles.ADMIN, OrgRoles.ISSUER, OrgRoles.VERIFIER, OrgRoles.MEMBER) + @ApiResponse({ status: HttpStatus.CREATED, description: 'Created', type: ApiResponseDto }) + async createConnectionInvitation( + @Param('orgId') orgId: string, + @Body() createOutOfBandConnectionInvitation: CreateOutOfBandConnectionInvitation, + @User() reqUser: IUserRequestInterface, + @Res() res: Response + ): Promise { + + createOutOfBandConnectionInvitation.orgId = orgId; + const connectionData = await this.connectionService.createConnectionInvitation(createOutOfBandConnectionInvitation, reqUser); + const finalResponse: IResponse = { + statusCode: HttpStatus.CREATED, + message: ResponseMessages.connection.success.create, + data: connectionData + }; + return res.status(HttpStatus.CREATED).json(finalResponse); + + } + + @Post('/orgs/:orgId/question-answer/question/:connectionId') + @ApiOperation({ summary: '', description: 'send question' }) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + @Roles(OrgRoles.OWNER, OrgRoles.ADMIN, OrgRoles.ISSUER, OrgRoles.VERIFIER, OrgRoles.MEMBER, OrgRoles.HOLDER, OrgRoles.SUPER_ADMIN, OrgRoles.PLATFORM_ADMIN) + @ApiResponse({ status: HttpStatus.CREATED, description: 'Created', type: ApiResponseDto }) + async sendQuestion( + @Param('orgId') orgId: string, + @Param('connectionId') connectionId: string, + @Body() questionDto: QuestionDto, + @User() reqUser: IUserRequestInterface, + @Res() res: Response + ): Promise { + + questionDto.orgId = orgId; + questionDto.connectionId = connectionId; + const questionData = await this.connectionService.sendQuestion(questionDto); + const finalResponse: IResponse = { + statusCode: HttpStatus.CREATED, + message: ResponseMessages.connection.success.questionSend, + data: questionData + }; + return res.status(HttpStatus.CREATED).json(finalResponse); + } @Post('/orgs/:orgId/receive-invitation-url') @@ -218,4 +328,37 @@ export class ConnectionController { } return res.status(HttpStatus.CREATED).json(finalResponse); } + + + @Post('wh/:orgId/question-answer/') + @ApiExcludeEndpoint() + @ApiOperation({ + summary: 'Catch question-answer webhook responses', + description: 'Callback URL for question-answer' + }) + @ApiResponse({ status: HttpStatus.CREATED, description: 'Created', type: ApiResponseDto }) + async getQuestionAnswerWebhook( + @Body() questionAnswerWebhookDto:QuestionAnswerWebhookDto, + @Param('orgId') orgId: string, + @Res() res: Response + ): Promise { + questionAnswerWebhookDto.type = 'question-answer'; + this.logger.debug(`questionAnswer ::: ${JSON.stringify(questionAnswerWebhookDto)} ${orgId}`); + + const finalResponse: IResponse = { + statusCode: HttpStatus.CREATED, + message: ResponseMessages.connection.success.create, + data: '' + }; + const webhookUrl = await this.connectionService._getWebhookUrl(questionAnswerWebhookDto.contextCorrelationId).catch(error => { + this.logger.debug(`error in getting webhook url ::: ${JSON.stringify(error)}`); + + }); + if (webhookUrl) { + await this.connectionService._postWebhookResponse(webhookUrl, { data: questionAnswerWebhookDto }).catch(error => { + this.logger.debug(`error in posting webhook response to webhook url ::: ${JSON.stringify(error)}`); + }); + } + return res.status(HttpStatus.CREATED).json(finalResponse); + } } diff --git a/apps/api-gateway/src/connection/connection.service.ts b/apps/api-gateway/src/connection/connection.service.ts index 0bb79af9a..3d5e6ac70 100644 --- a/apps/api-gateway/src/connection/connection.service.ts +++ b/apps/api-gateway/src/connection/connection.service.ts @@ -2,10 +2,11 @@ import { IUserRequest } from '@credebl/user-request/user-request.interface'; import { Inject, Injectable} from '@nestjs/common'; import { ClientProxy, RpcException } from '@nestjs/microservices'; import { BaseService } from 'libs/service/base.service'; -import { ConnectionDto, CreateConnectionDto, ReceiveInvitationDto, ReceiveInvitationUrlDto } from './dtos/connection.dto'; +import { ConnectionDto, CreateConnectionDto, CreateOutOfBandConnectionInvitation, ReceiveInvitationDto, ReceiveInvitationUrlDto } from './dtos/connection.dto'; import { IReceiveInvitationRes, IUserRequestInterface } from './interfaces'; import { IConnectionList, ICreateConnectionUrl } from '@credebl/common/interfaces/connection.interface'; -import { IConnectionDetailsById, IConnectionSearchCriteria } from '../interfaces/IConnectionSearch.interface'; +import { AgentConnectionSearchCriteria, IConnectionDetailsById, IConnectionSearchCriteria } from '../interfaces/IConnectionSearch.interface'; +import { QuestionDto } from './dtos/question-answer.dto'; @Injectable() export class ConnectionService extends BaseService { @@ -13,6 +14,16 @@ export class ConnectionService extends BaseService { super('ConnectionService'); } + sendQuestion( + questionDto: QuestionDto + ): Promise { + try { + return this.sendNatsMessage(this.connectionServiceProxy, 'send-question', questionDto); + } catch (error) { + throw new RpcException(error.response); + } + } + createLegacyConnectionInvitation( connectionDto: CreateConnectionDto, user: IUserRequestInterface @@ -29,7 +40,8 @@ export class ConnectionService extends BaseService { goal: connectionDto.goal, handshake: connectionDto.handshake, handshakeProtocols: connectionDto.handshakeProtocols, - user + user, + recipientKey:connectionDto.recipientKey }; return this.sendNatsMessage(this.connectionServiceProxy, 'create-connection', connectionDetails); @@ -66,6 +78,14 @@ export class ConnectionService extends BaseService { return this.sendNatsMessage(this.connectionServiceProxy, 'get-all-connections', payload); } + getConnectionListFromAgent( + connectionSearchCriteria: AgentConnectionSearchCriteria, + orgId: string + ): Promise { + const payload = { connectionSearchCriteria, orgId }; + return this.sendNatsMessage(this.connectionServiceProxy, 'get-all-agent-connection-list', payload); + } + getConnectionsById( user: IUserRequest, connectionId: string, @@ -75,6 +95,14 @@ export class ConnectionService extends BaseService { return this.sendNatsMessage(this.connectionServiceProxy, 'get-connection-details-by-connectionId', payload); } + + getQuestionAnswersRecord( + orgId: string + ): Promise { + + return this.sendNatsMessage(this.connectionServiceProxy, 'get-question-answer-record', orgId); + } + receiveInvitationUrl( receiveInvitationUrl: ReceiveInvitationUrlDto, orgId: string, @@ -121,4 +149,11 @@ export class ConnectionService extends BaseService { } } + createConnectionInvitation( + createOutOfBandConnectionInvitation: CreateOutOfBandConnectionInvitation, + user: IUserRequestInterface + ): Promise { + const payload = { user, createOutOfBandConnectionInvitation }; + return this.sendNatsMessage(this.connectionServiceProxy, 'create-connection-invitation', payload); + } } diff --git a/apps/api-gateway/src/connection/dtos/connection.dto.ts b/apps/api-gateway/src/connection/dtos/connection.dto.ts index 6ac0f90fc..afe499c6a 100644 --- a/apps/api-gateway/src/connection/dtos/connection.dto.ts +++ b/apps/api-gateway/src/connection/dtos/connection.dto.ts @@ -2,6 +2,59 @@ import { ArrayNotEmpty, IsArray, IsBoolean, IsNotEmpty, IsNumber, IsOptional, Is import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { Type } from 'class-transformer'; +import { HandshakeProtocol } from '../enums/connections.enum'; + +export class CreateOutOfBandConnectionInvitation { + @ApiPropertyOptional() + @IsOptional() + label?: string; + + @ApiPropertyOptional() + @IsOptional() + alias?: string; + + @ApiPropertyOptional() + @IsOptional() + imageUrl?: string; + + @ApiPropertyOptional() + @IsOptional() + goalCode?: string; + + @ApiPropertyOptional() + @IsOptional() + goal?: string; + + @ApiPropertyOptional() + @IsOptional() + handshake?: boolean; + + @ApiPropertyOptional() + @IsOptional() + handshakeProtocols?: HandshakeProtocol[]; + + @ApiPropertyOptional() + @IsOptional() + messages?: object[]; + + @ApiPropertyOptional() + @IsOptional() + multiUseInvitation?: boolean; + + @ApiPropertyOptional() + @IsOptional() + autoAcceptConnection?: boolean; + + @ApiPropertyOptional() + @IsOptional() + routing?: object; + + @ApiPropertyOptional() + @IsOptional() + appendedAttachments?: object[]; + + orgId; +} export class CreateConnectionDto { @ApiPropertyOptional() @@ -60,6 +113,12 @@ export class CreateConnectionDto { handshakeProtocols: string[]; orgId: string; + + @ApiPropertyOptional() + @IsString() + @IsOptional() + @IsNotEmpty({ message: 'Please provide recipientKey' }) + recipientKey: string; } export class ConnectionDto { diff --git a/apps/api-gateway/src/connection/dtos/get-all-connections.dto.ts b/apps/api-gateway/src/connection/dtos/get-all-connections.dto.ts index 553a8a5a2..ac5ddb329 100644 --- a/apps/api-gateway/src/connection/dtos/get-all-connections.dto.ts +++ b/apps/api-gateway/src/connection/dtos/get-all-connections.dto.ts @@ -1,41 +1,61 @@ -import { ApiProperty } from "@nestjs/swagger"; -import { Transform, Type } from "class-transformer"; -import { IsEnum, IsOptional } from "class-validator"; -import { SortValue } from "../../enum"; -import { trim } from "@credebl/common/cast.helper"; -import { SortFields } from "apps/connection/src/enum/connection.enum"; +import { ApiProperty } from '@nestjs/swagger'; +import { Transform, Type } from 'class-transformer'; +import { IsEnum, IsOptional } from 'class-validator'; +import { SortValue } from '../../enum'; +import { trim } from '@credebl/common/cast.helper'; +import { SortFields } from 'apps/connection/src/enum/connection.enum'; export class GetAllConnectionsDto { - - @ApiProperty({ required: false, example: '1' }) - @IsOptional() - pageNumber: number = 1; - - @ApiProperty({ required: false, example: '10' }) - @IsOptional() - pageSize: number = 10; - - @ApiProperty({ required: false }) - @IsOptional() - @Transform(({ value }) => trim(value)) - @Type(() => String) - searchByText: string = ''; - - @ApiProperty({ - required: false - }) - @Transform(({ value }) => trim(value)) - @IsOptional() - @IsEnum(SortFields) - sortField: string = SortFields.CREATED_DATE_TIME; - - @ApiProperty({ - enum: [SortValue.DESC, SortValue.ASC], - required: false - }) - @Transform(({ value }) => trim(value)) - @IsOptional() - @IsEnum(SortValue) - sortBy: string = SortValue.DESC; + @ApiProperty({ required: false, example: '1' }) + @IsOptional() + pageNumber: number = 1; + + @ApiProperty({ required: false, example: '10' }) + @IsOptional() + pageSize: number = 10; + + @ApiProperty({ required: false }) + @IsOptional() + @Transform(({ value }) => trim(value)) + @Type(() => String) + searchByText: string = ''; + + @ApiProperty({ + required: false + }) + @Transform(({ value }) => trim(value)) + @IsOptional() + @IsEnum(SortFields) + sortField: string = SortFields.CREATED_DATE_TIME; + + @ApiProperty({ + enum: [SortValue.DESC, SortValue.ASC], + required: false + }) + @Transform(({ value }) => trim(value)) + @IsOptional() + @IsEnum(SortValue) + sortBy: string = SortValue.DESC; } +export class GetAllAgentConnectionsDto { + @ApiProperty({ required: false, example: 'e315f30d-9beb-4068-aea4-abb5fe5eecb1' }) + @IsOptional() + outOfBandId: string = ''; + + @ApiProperty({ required: false, example: 'Test' }) + @IsOptional() + alias: string = ''; + + @ApiProperty({ required: false, example: 'did:example:e315f30d-9beb-4068-aea4-abb5fe5eecb1' }) + @IsOptional() + myDid: string = ''; + + @ApiProperty({ required: false, example: 'did:example:e315f30d-9beb-4068-aea4-abb5fe5eecb1' }) + @IsOptional() + theirDid: string = ''; + + @ApiProperty({ required: false, example: 'Bob' }) + @IsOptional() + theirLabel: string = ''; +} diff --git a/apps/api-gateway/src/connection/dtos/question-answer.dto.ts b/apps/api-gateway/src/connection/dtos/question-answer.dto.ts new file mode 100644 index 000000000..583448183 --- /dev/null +++ b/apps/api-gateway/src/connection/dtos/question-answer.dto.ts @@ -0,0 +1,94 @@ +import { trim } from '@credebl/common/cast.helper'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { Transform, Type } from 'class-transformer'; +import { IsArray, IsNotEmpty, IsOptional, IsString, ValidateNested } from 'class-validator'; + +class ValidResponses { + @ApiProperty({ example: 'Emma' }) + @IsNotEmpty({ message: 'text is required' }) + @IsString({ message: 'text should be a string' }) + @Transform(({ value }) => trim(value)) + @Type(() => String) + text: string; +} +export class QuestionDto { + @ApiPropertyOptional() + @IsOptional() + @IsString({ message: 'detail must be a string' }) + @IsNotEmpty({ message: 'please provide valid detail' }) + detail: string; + + @ApiProperty({ example: [{ 'text': 'Emma'}, { 'text': 'Kiva'}] }) + @IsNotEmpty({ message: 'Please provide valid responses' }) + @IsArray({ message: 'Responses should be array' }) + @ValidateNested({ each: true }) + @Type(() => ValidResponses) + validResponses: ValidResponses[]; + + @ApiProperty({ example: 'What is your name'}) + @IsNotEmpty({ message: 'question is required' }) + @IsString({ message: 'question must be a string' }) + @IsNotEmpty({ message: 'please provide valid question' }) + question: string; + + orgId: string; + connectionId: string; +} + +export class QuestionAnswerWebhookDto { + + + @ApiPropertyOptional() + @IsOptional() + id: string; + + @ApiPropertyOptional() + @IsOptional() + createdAt: string; + + @ApiPropertyOptional() + @IsOptional() + questionText: string; + + @ApiPropertyOptional() + @IsOptional() + questionDetail: string; + + @ApiPropertyOptional() + @IsOptional() + // eslint-disable-next-line @typescript-eslint/no-explicit-any + validResponses:any; + + @ApiPropertyOptional() + @IsOptional() + connectionId: string; + + @ApiPropertyOptional() + @IsOptional() + role: string; + + @ApiPropertyOptional() + @IsOptional() + signatureRequired: boolean; + + @ApiPropertyOptional() + @IsOptional() + state: boolean; + + @ApiPropertyOptional() + @IsOptional() + threadId: string; + + @ApiPropertyOptional() + @IsOptional() + updatedAt: string; + + @ApiPropertyOptional() + @IsOptional() + contextCorrelationId: string; + + @ApiPropertyOptional() + @IsOptional() + type: string; + +} \ No newline at end of file diff --git a/apps/api-gateway/src/connection/enums/connections.enum.ts b/apps/api-gateway/src/connection/enums/connections.enum.ts index 3ea871b2a..1c4a24600 100644 --- a/apps/api-gateway/src/connection/enums/connections.enum.ts +++ b/apps/api-gateway/src/connection/enums/connections.enum.ts @@ -9,4 +9,9 @@ export enum Connections { responseReceived = 'response-received', complete = 'complete', abandoned = 'abandoned' +} + +export declare enum HandshakeProtocol { + Connections = "https://didcomm.org/connections/1.0", + DidExchange = "https://didcomm.org/didexchange/1.0" } \ No newline at end of file diff --git a/apps/api-gateway/src/dtos/create-schema.dto.ts b/apps/api-gateway/src/dtos/create-schema.dto.ts index 7b83fbd9f..380b797a3 100644 --- a/apps/api-gateway/src/dtos/create-schema.dto.ts +++ b/apps/api-gateway/src/dtos/create-schema.dto.ts @@ -1,4 +1,4 @@ -import { IsArray, IsNotEmpty, IsOptional, IsString, ValidateNested } from 'class-validator'; +import { ArrayMinSize, IsArray, IsBoolean, IsNotEmpty, IsOptional, IsString, ValidateNested } from 'class-validator'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { Transform, Type } from 'class-transformer'; @@ -23,6 +23,11 @@ class AttributeValue { @Transform(({ value }) => trim(value)) @IsNotEmpty({ message: 'displayName is required' }) displayName: string; + + @ApiProperty() + @IsBoolean() + @IsNotEmpty({ message: 'isRequired property is required' }) + isRequired: boolean; } export class CreateSchemaDto { @@ -44,12 +49,14 @@ export class CreateSchemaDto { { attributeName: 'name', schemaDataType: 'string', - displayName: 'Name' + displayName: 'Name', + isRequired: true } ] }) @IsArray({ message: 'attributes must be an array' }) @IsNotEmpty({ message: 'attributes are required' }) + @ArrayMinSize(1) @ValidateNested({ each: true }) @Type(() => AttributeValue) attributes: AttributeValue[]; @@ -63,3 +70,46 @@ export class CreateSchemaDto { @IsString({ message: 'orgDid must be a string' }) orgDid: string; } + +export class CreateW3CSchemaDto { + @ApiProperty({ + type: [], + 'example': [ + { + title: 'name', + type: 'string' + } + ] + }) + @IsNotEmpty({ message: 'Schema attributes are required' }) + schemaAttributes: SchemaAttributes []; + + @ApiProperty() + @IsString({ message: 'schemaName must be a string' }) + @Transform(({ value }) => value.trim()) + @IsNotEmpty({ message: 'schemaName is required' }) + schemaName: string; + + @ApiProperty() + @IsString({ message: 'did must be a string' }) + @Transform(({ value }) => value.trim()) + @IsNotEmpty({ message: 'did is required' }) + did: string; + + @ApiProperty() + @IsString({ message: 'description must be a string' }) + @IsNotEmpty({ message: 'description is required' }) + description: string; +} + +export class SchemaAttributes { + @ApiProperty() + @IsNotEmpty({ message: 'type is required' }) + @IsString({ message: 'type must be a string' }) + type: string; + + @ApiProperty() + @IsNotEmpty({ message: 'title is required' }) + @IsString({ message: 'title must be a string' }) + title: string; +} \ No newline at end of file diff --git a/apps/api-gateway/src/ecosystem/dtos/create-ecosystem-dto.ts b/apps/api-gateway/src/ecosystem/dtos/create-ecosystem-dto.ts index 03a49fcaa..5b5e37fbc 100644 --- a/apps/api-gateway/src/ecosystem/dtos/create-ecosystem-dto.ts +++ b/apps/api-gateway/src/ecosystem/dtos/create-ecosystem-dto.ts @@ -9,10 +9,10 @@ export class CreateEcosystemDto { @ApiProperty() @Transform(({ value }) => trim(value)) - @IsNotEmpty({ message: 'ecosystem name is required.' }) - @MinLength(2, { message: 'ecosystem name must be at least 2 characters.' }) - @MaxLength(50, { message: 'ecosystem name must be at most 50 characters.' }) - @IsString({ message: 'ecosystem name must be in string format.' }) + @IsNotEmpty({ message: 'Ecosystem name is required.' }) + @MinLength(2, { message: 'Ecosystem name must be at least 2 characters.' }) + @MaxLength(50, { message: 'Ecosystem name must be at most 50 characters.' }) + @IsString({ message: 'Ecosystem name must be in string format.' }) name: string; @ApiProperty() diff --git a/apps/api-gateway/src/ecosystem/dtos/get-members.dto.ts b/apps/api-gateway/src/ecosystem/dtos/get-members.dto.ts index 3a7e78f78..f12af3ea4 100644 --- a/apps/api-gateway/src/ecosystem/dtos/get-members.dto.ts +++ b/apps/api-gateway/src/ecosystem/dtos/get-members.dto.ts @@ -1,34 +1,20 @@ -import { Transform, Type } from 'class-transformer'; +import { Transform } from 'class-transformer'; import { trim } from '@credebl/common/cast.helper'; import { ApiProperty } from '@nestjs/swagger'; import { IsEnum, IsOptional } from 'class-validator'; -import { SortFields } from 'apps/connection/src/enum/connection.enum'; -import { SortValue } from '@credebl/enum/enum'; - -export class GetAllEcosystemMembersDto { - - @ApiProperty({ required: false, example: '1' }) - @IsOptional() - pageNumber: number; - - @ApiProperty({ required: false, example: '10' }) - @IsOptional() - pageSize: number; - - @ApiProperty({ required: false }) - @IsOptional() - @Transform(({ value }) => trim(value)) - @Type(() => String) - search: string = ''; +import { SortMembers, SortValue } from '@credebl/enum/enum'; +import { PaginationDto } from '@credebl/common/dtos/pagination.dto'; +export class GetAllEcosystemMembersDto extends PaginationDto { @ApiProperty({ + enum: [SortMembers.CREATED_DATE_TIME, SortMembers.ID, SortMembers.ORGANIZATION, SortMembers.STATUS], required: false }) @Transform(({ value }) => trim(value)) @IsOptional() - @IsEnum(SortFields) - sortField: string = SortFields.CREATED_DATE_TIME; + @IsEnum(SortMembers) + sortField: string = SortMembers.CREATED_DATE_TIME; @ApiProperty({ enum: [SortValue.DESC, SortValue.ASC], @@ -38,5 +24,4 @@ export class GetAllEcosystemMembersDto { @IsOptional() @IsEnum(SortValue) sortBy: string = SortValue.DESC; - } diff --git a/apps/api-gateway/src/ecosystem/dtos/request-schema.dto.ts b/apps/api-gateway/src/ecosystem/dtos/request-schema.dto.ts index f39e29f0c..708985dda 100644 --- a/apps/api-gateway/src/ecosystem/dtos/request-schema.dto.ts +++ b/apps/api-gateway/src/ecosystem/dtos/request-schema.dto.ts @@ -1,24 +1,34 @@ -import { ApiExtraModels, ApiProperty } from '@nestjs/swagger'; +import { ApiProperty } from '@nestjs/swagger'; import { Transform, Type } from 'class-transformer'; -import { IsArray, IsBoolean, IsNotEmpty, IsOptional, IsString, ValidateNested } from 'class-validator'; +import { ArrayMinSize, IsArray, IsBoolean, IsNotEmpty, IsOptional, IsString, ValidateNested } from 'class-validator'; import { trim } from '@credebl/common/cast.helper'; -@ApiExtraModels() - -class AttributeValue { +class AttributeValues { + @ApiProperty() @IsString() - @IsNotEmpty({ message: 'attributeName is required.' }) + @Transform(({ value }) => trim(value)) + @IsNotEmpty({ message: 'attributeName is required' }) attributeName: string; + @ApiProperty() @IsString() - @IsNotEmpty({ message: 'schemaDataType is required.' }) - schemaDataType: string; + @Transform(({ value }) => trim(value)) + @IsNotEmpty({ message: 'displayName is required' }) + displayName: string; + @ApiProperty() + @IsBoolean() + @IsNotEmpty({ message: 'isRequired property is required' }) + isRequired: boolean; + + @ApiProperty() @IsString() - @IsNotEmpty({ message: 'displayName is required.' }) - displayName: string; + @Transform(({ value }) => trim(value)) + @IsNotEmpty({ message: 'schemaDataType is required' }) + schemaDataType: string; + } @@ -37,17 +47,22 @@ export class RequestSchemaDto { version: string; @ApiProperty({ + type: [AttributeValues], 'example': [ { attributeName: 'name', schemaDataType: 'string', - displayName: 'Name' + displayName: 'Name', + isRequired: true } ] }) @IsArray({ message: 'attributes must be an array' }) - @IsNotEmpty({ message: 'please provide valid attributes' }) - attributes: AttributeValue[]; + @IsNotEmpty({ message: 'attributes are required' }) + @ArrayMinSize(1) + @ValidateNested({ each: true }) + @Type(() => AttributeValues) + attributes: AttributeValues[]; @ApiProperty() @IsBoolean({ message: 'endorse must be a boolean.' }) diff --git a/apps/api-gateway/src/ecosystem/ecosystem.controller.ts b/apps/api-gateway/src/ecosystem/ecosystem.controller.ts index bfc3371c8..489e5df84 100644 --- a/apps/api-gateway/src/ecosystem/ecosystem.controller.ts +++ b/apps/api-gateway/src/ecosystem/ecosystem.controller.ts @@ -1,6 +1,6 @@ import { ApiBearerAuth, ApiExcludeEndpoint, ApiForbiddenResponse, ApiOperation, ApiQuery, ApiResponse, ApiTags, ApiUnauthorizedResponse } from '@nestjs/swagger'; import { EcosystemService } from './ecosystem.service'; -import { Controller, UseFilters, Put, Post, Get, Body, Param, UseGuards, Query, BadRequestException, Delete, HttpStatus, Res } from '@nestjs/common'; +import { Controller, UseFilters, Put, Post, Get, Body, Param, UseGuards, Query, BadRequestException, Delete, HttpStatus, Res, ParseUUIDPipe } from '@nestjs/common'; import { RequestCredDefDto, RequestSchemaDto } from './dtos/request-schema.dto'; import IResponse from '@credebl/common/interfaces/response.interface'; import { Response } from 'express'; @@ -110,7 +110,7 @@ export class EcosystemController { const finalResponse: IResponse = { statusCode: 200, message: ResponseMessages.ecosystem.success.allschema, - data: schemaList.response + data: schemaList }; return res.status(200).json(finalResponse); } @@ -124,11 +124,27 @@ export class EcosystemController { @UseGuards(AuthGuard('jwt'), OrgRolesGuard) @Roles(OrgRoles.OWNER, OrgRoles.ADMIN, OrgRoles.ISSUER, OrgRoles.VERIFIER, OrgRoles.MEMBER) @ApiBearerAuth() + @ApiQuery({ + name: 'pageNumber', + type: Number, + required: false + }) + @ApiQuery({ + name: 'pageSize', + type: Number, + required: false + }) + @ApiQuery({ + name: 'search', + type: String, + required: false + }) async getEcosystem( + @Query() paginationDto: PaginationDto, @Param('orgId') orgId: string, @Res() res: Response ): Promise { - const ecosystemList = await this.ecosystemService.getAllEcosystem(orgId); + const ecosystemList = await this.ecosystemService.getAllEcosystem(orgId, paginationDto); const finalResponse: IResponse = { statusCode: 200, message: ResponseMessages.ecosystem.success.fetch, @@ -256,7 +272,7 @@ export class EcosystemController { @EcosystemsRoles(EcosystemRoles.ECOSYSTEM_OWNER, EcosystemRoles.ECOSYSTEM_LEAD, EcosystemRoles.ECOSYSTEM_MEMBER) @ApiBearerAuth() @UseGuards(AuthGuard('jwt'), EcosystemRolesGuard, OrgRolesGuard) - @ApiResponse({ status: 200, description: 'Success', type: ApiResponseDto }) + @ApiResponse({ status: HttpStatus.OK, description: 'Success', type: ApiResponseDto }) @ApiOperation({ summary: 'Get ecosystem members list', description: 'Get ecosystem members list.' }) @ApiQuery({ name: 'pageNumber', @@ -274,18 +290,19 @@ export class EcosystemController { required: false }) async getEcosystemMembers( - @Param('ecosystemId') ecosystemId: string, + @Param('ecosystemId', new ParseUUIDPipe({exceptionFactory: (): Error => { throw new BadRequestException(`Invalid format for ecosystemId`); }})) ecosystemId: string, @Param('orgId') orgId: string, @Query() getEcosystemMembers: GetAllEcosystemMembersDto, @Res() res: Response): Promise { + const members = await this.ecosystemService.getEcosystemMembers(ecosystemId, getEcosystemMembers); const finalResponse: IResponse = { - statusCode: 200, + statusCode: HttpStatus.OK, message: ResponseMessages.ecosystem.success.fetchMembers, - data: members?.response + data: members }; - return res.status(200).json(finalResponse); + return res.status(HttpStatus.OK).json(finalResponse); } @Post('/:ecosystemId/:orgId/transaction/schema') diff --git a/apps/api-gateway/src/ecosystem/ecosystem.service.ts b/apps/api-gateway/src/ecosystem/ecosystem.service.ts index 2494fee1b..6d5ae7cb3 100644 --- a/apps/api-gateway/src/ecosystem/ecosystem.service.ts +++ b/apps/api-gateway/src/ecosystem/ecosystem.service.ts @@ -11,8 +11,9 @@ import { GetAllEndorsementsDto } from './dtos/get-all-endorsements.dto'; import { RequestSchemaDto, RequestCredDefDto } from './dtos/request-schema.dto'; import { CreateEcosystemDto } from './dtos/create-ecosystem-dto'; import { EditEcosystemDto } from './dtos/edit-ecosystem-dto'; -import { IEcosystemDashboard, EcosystemDetailsResult, IEcosystemInvitation, IEcosystemInvitations, IEcosystem, IEditEcosystem, IEndorsementTransaction } from 'apps/ecosystem/interfaces/ecosystem.interfaces'; +import { IEcosystemDashboard, IEcosystemInvitation, IEcosystemInvitations, IEcosystem, IEditEcosystem, IEndorsementTransaction, ISchemaResponse } from 'apps/ecosystem/interfaces/ecosystem.interfaces'; import { PaginationDto } from '@credebl/common/dtos/pagination.dto'; +import { IEcosystemDetails } from '@credebl/common/interfaces/ecosystem.interface'; @Injectable() export class EcosystemService extends BaseService { @@ -45,8 +46,8 @@ export class EcosystemService extends BaseService { * * @returns Get all ecosystems */ - async getAllEcosystem(orgId: string): Promise { - const payload = { orgId }; + async getAllEcosystem(orgId: string, payload: PaginationDto): Promise { + payload['orgId'] = orgId; return this.sendNatsMessage(this.serviceProxy, 'get-all-ecosystem', payload); } @@ -93,11 +94,10 @@ export class EcosystemService extends BaseService { */ async getEcosystemMembers( ecosystemId: string, - getEcosystemMembers: GetAllEcosystemMembersDto + payload: GetAllEcosystemMembersDto ): Promise<{ response: object }> { - const { pageNumber, pageSize, search, sortBy } = getEcosystemMembers; - const payload = { ecosystemId, pageNumber, pageSize, search, sortBy }; - return this.sendNats(this.serviceProxy, 'fetch-ecosystem-members', payload); + payload['ecosystemId'] = ecosystemId; + return this.sendNatsMessage(this.serviceProxy, 'fetch-ecosystem-members', payload); } /** @@ -126,9 +126,9 @@ export class EcosystemService extends BaseService { return this.sendNats(this.serviceProxy, 'accept-reject-ecosystem-invitations', payload); } - async fetchEcosystemOrg(ecosystemId: string, orgId: string): Promise<{ response: object }> { + async fetchEcosystemOrg(ecosystemId: string, orgId: string): Promise { const payload = { ecosystemId, orgId }; - return this.sendNats(this.serviceProxy, 'fetch-ecosystem-org-data', payload); + return this.sendNatsMessage(this.serviceProxy, 'fetch-ecosystem-org-data', payload); } async getEndorsementTranasactions( @@ -145,7 +145,7 @@ export class EcosystemService extends BaseService { ecosystemId: string, orgId: string, paginationDto: PaginationDto - ): Promise<{ response: object }> { + ): Promise { const { pageNumber, pageSize, search } = paginationDto; const payload = { ecosystemId, orgId, pageNumber, pageSize, search }; return this.sendNatsMessage(this.serviceProxy, 'get-all-ecosystem-schemas', payload); diff --git a/apps/api-gateway/src/interfaces/IConnectionSearch.interface.ts b/apps/api-gateway/src/interfaces/IConnectionSearch.interface.ts index 35d79949d..a228ae636 100644 --- a/apps/api-gateway/src/interfaces/IConnectionSearch.interface.ts +++ b/apps/api-gateway/src/interfaces/IConnectionSearch.interface.ts @@ -1,25 +1,34 @@ import { IUserRequestInterface } from './IUserRequestInterface'; export interface IConnectionSearchCriteria { - pageNumber: number; - pageSize: number; - sortField: string; - sortBy: string; - searchByText: string; - user?: IUserRequestInterface + pageNumber: number; + pageSize: number; + sortField: string; + sortBy: string; + searchByText: string; + user?: IUserRequestInterface; } export interface IConnectionDetailsById { - id: string; - createdAt: string; - did: string; - theirDid: string; - theirLabel: string; - state: string; - role: string; - autoAcceptConnection: boolean; - threadId: string; - protocol: string; - outOfBandId: string; - updatedAt: string; - } + id: string; + createdAt: string; + did: string; + theirDid: string; + theirLabel: string; + state: string; + role: string; + autoAcceptConnection: boolean; + threadId: string; + protocol: string; + outOfBandId: string; + updatedAt: string; +} + +export interface AgentConnectionSearchCriteria { + outOfBandId?: string; + alias?: string; + state?: string; + myDid?: string; + theirDid?: string; + theirLabel?: string; +} diff --git a/apps/api-gateway/src/interfaces/ISchemaSearch.interface.ts b/apps/api-gateway/src/interfaces/ISchemaSearch.interface.ts index 2f83d26cb..51c10b237 100644 --- a/apps/api-gateway/src/interfaces/ISchemaSearch.interface.ts +++ b/apps/api-gateway/src/interfaces/ISchemaSearch.interface.ts @@ -10,3 +10,14 @@ export interface ISchemaSearchPayload { user?: IUserRequestInterface } + +export interface W3CSchemaPayload { + schemaAttributes: W3CSchemaAttributes []; + schemaName: string; + did: string; + } + + interface W3CSchemaAttributes { + type: string, + title: string + } diff --git a/apps/api-gateway/src/interfaces/IUserRequestInterface.ts b/apps/api-gateway/src/interfaces/IUserRequestInterface.ts index 908cfe524..dbda90cec 100644 --- a/apps/api-gateway/src/interfaces/IUserRequestInterface.ts +++ b/apps/api-gateway/src/interfaces/IUserRequestInterface.ts @@ -1,6 +1,7 @@ import { UserRoleOrgPermsDto } from '../authz/dtos/user-role-org-perms.dto'; export interface IUserRequestInterface { + id: string; userId: string; email: string; orgId: string; diff --git a/apps/api-gateway/src/issuance/dtos/get-all-issued-credentials.dto.ts b/apps/api-gateway/src/issuance/dtos/get-all-issued-credentials.dto.ts index 9e07e5ecf..37be22275 100644 --- a/apps/api-gateway/src/issuance/dtos/get-all-issued-credentials.dto.ts +++ b/apps/api-gateway/src/issuance/dtos/get-all-issued-credentials.dto.ts @@ -1,29 +1,12 @@ import { ApiProperty } from '@nestjs/swagger'; -import { Transform, Type } from 'class-transformer'; -import { IsEnum, IsInt, IsOptional, IsString } from 'class-validator'; +import { Transform } from 'class-transformer'; +import { IsEnum, IsOptional } from 'class-validator'; import { SortValue } from '../../enum'; import { trim } from '@credebl/common/cast.helper'; import { SortFields } from 'apps/issuance/enum/issuance.enum'; +import { PaginationDto } from '@credebl/common/dtos/pagination.dto'; -export class IGetAllIssuedCredentialsDto { - @ApiProperty({ required: false, default: 1 }) - @IsOptional() - @Type(() => Number) - @IsInt({ message: 'Page Number should be a number' }) - pageNumber: number = 1; - - @ApiProperty({ required: false, default: 10 }) - @IsOptional() - @Type(() => Number) - @IsInt({ message: 'Page size should be a number' }) - pageSize: number; - - @ApiProperty({ required: false }) - @IsOptional() - @Type(() => String) - @IsString({ message: 'Search text should be a string' }) - @Transform(({ value }) => trim(value)) - searchByText: string; +export class IGetAllIssuedCredentialsDto extends PaginationDto { @ApiProperty({ required: false, enum: SortFields }) @Transform(({ value }) => trim(value)) diff --git a/apps/api-gateway/src/issuance/dtos/issuance.dto.ts b/apps/api-gateway/src/issuance/dtos/issuance.dto.ts index c51deb603..20c1ba466 100644 --- a/apps/api-gateway/src/issuance/dtos/issuance.dto.ts +++ b/apps/api-gateway/src/issuance/dtos/issuance.dto.ts @@ -1,12 +1,110 @@ +/* eslint-disable @typescript-eslint/array-type */ -import { IsArray, IsNotEmpty, IsOptional, IsString, IsEmail, ArrayMaxSize, ValidateNested, ArrayMinSize, IsBoolean, IsDefined, MaxLength, IsEnum } from 'class-validator'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { ArrayMaxSize, ArrayMinSize, IsArray, IsBoolean, IsDefined, IsEmail, IsEnum, IsNotEmpty, IsObject, IsOptional, IsString, MaxLength, ValidateNested } from 'class-validator'; +import { IsCredentialJsonLdContext, SingleOrArray } from '../utils/helper'; +import { IssueCredentialType, JsonLdCredentialDetailCredentialStatusOptions, JsonLdCredentialDetailOptionsOptions, JsonObject } from '../interfaces'; 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 { SortFields } from 'apps/connection/src/enum/connection.enum'; +import { SortValue } from '../../enum'; +import { trim } from '@credebl/common/cast.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; + + @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; + [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' }) @@ -19,6 +117,13 @@ class Attribute { @IsDefined() @Transform(({ value }) => trim(value)) value: string; + + @ApiProperty({ default: false }) + @IsBoolean() + @IsOptional() + @IsNotEmpty({ message: 'isRequired property is required' }) + isRequired?: boolean = false; + } class CredentialsIssuanceDto { @@ -26,7 +131,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' }) @@ -64,6 +170,12 @@ class CredentialsIssuanceDto { @IsString({ message: 'label should be string' }) label?: string; + @ApiPropertyOptional() + @IsOptional() + @IsNotEmpty({ message: 'please provide valid imageUrl' }) + @IsString({ message: 'imageUrl must be a string' }) + imageUrl?: string; + @ApiPropertyOptional() @IsOptional() @IsString({ message: 'auto accept proof must be in string' }) @@ -73,17 +185,64 @@ class CredentialsIssuanceDto { }) autoAcceptCredential?: string; + @ApiProperty({ example: 'jsonld' }) + @IsNotEmpty({ message: 'Please provide credential type ' }) + @Transform(({ value }) => trim(value).toLocaleLowerCase()) + @IsOptional() + credentialType:IssueCredentialType; + + @ApiPropertyOptional({ default: true }) + @IsOptional() + @IsNotEmpty({ message: 'please provide valid value for reuseConnection' }) + @IsBoolean({ message: 'reuseConnection must be a boolean' }) + reuseConnection?: boolean; + orgId: string; } export class OOBIssueCredentialDto extends CredentialsIssuanceDto { + @ApiProperty({ + example: [ + { + value: 'string', + name: 'string' + } + ] + }) + @IsArray() + @ValidateNested({ each: true }) + @ArrayMinSize(1) + @IsOptional() + @IsNotEmpty({ message: 'Please provide valid attributes' }) + @Type(() => Attribute) + attributes?: Attribute[]; + + @ApiProperty({ + example: false + }) + @IsOptional() + @IsNotEmpty() + @IsBoolean({message: 'isShortenUrl must be boolean'}) + isShortenUrl?: boolean; + + + @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; - @ApiProperty({ example: [{ 'value': 'string', 'name': 'string' }] }) - @IsArray() - @ValidateNested({ each: true }) - @ArrayMinSize(1) - @Type(() => Attribute) - attributes: Attribute[]; } class CredentialOffer { @@ -92,9 +251,10 @@ class CredentialOffer { @IsArray({ message: 'Attributes should be an array' }) @ValidateNested({ each: true }) @Type(() => Attribute) - attributes: Attribute[]; + @IsOptional() + attributes?: Attribute[]; - @ApiProperty({ example: 'testmail@mailinator.com' }) + @ApiProperty({ example: 'testmail@xyz.com' }) @IsEmail({}, { message: 'Please provide a valid email' }) @IsNotEmpty({ message: 'Email is required' }) @IsString({ message: 'Email should be a string' }) @@ -102,6 +262,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 { @@ -199,7 +375,39 @@ export class CredentialAttributes { } export class OOBCredentialDtoWithEmail { - @ApiProperty({ example: [{ 'emailId': 'abc@example.com', 'attributes': [{ 'value': 'string', 'name': 'string' }] }] }) + @ApiProperty({ example: [ + { + 'emailId': 'xyz@example.com', + '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` }) @@ -207,19 +415,12 @@ export class OOBCredentialDtoWithEmail { @Type(() => CredentialOffer) credentialOffer: CredentialOffer[]; - @ApiProperty({ example: 'awqx@getnada.com' }) - @IsEmail({}, { message: 'Please provide a valid email' }) - @IsNotEmpty({ message: 'Please provide valid email' }) - @IsString({ message: 'email should be string' }) - @Transform(({ value }) => value.trim().toLowerCase()) - @IsOptional() - emailId: string; - @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() @@ -233,6 +434,14 @@ 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; } @@ -309,6 +518,16 @@ export class ClientDetails { @Type(() => String) clientId = ''; - userId?: string; + @ApiProperty({ required: false, example: 'issue-data.csv' }) + @IsOptional() + @Type(() => String) + fileName = ''; + @ApiProperty({ required: false }) + @IsOptional() + @Type(() => Boolean) + isSelectiveIssuance?: boolean = false; + + userId?: string; + } \ No newline at end of file diff --git a/apps/api-gateway/src/issuance/interfaces/index.ts b/apps/api-gateway/src/issuance/interfaces/index.ts index 834a33b4d..107c08992 100644 --- a/apps/api-gateway/src/issuance/interfaces/index.ts +++ b/apps/api-gateway/src/issuance/interfaces/index.ts @@ -1,3 +1,6 @@ +import { JsonLdCredentialDetailCredentialStatus } from '../dtos/issuance.dto'; +import { JsonValue } from '../utils/helper'; + export interface IUserRequestInterface { userId: string; email: string; @@ -72,6 +75,27 @@ export interface IIssuedCredentialSearchParams { pageSize: number; sortField: string; sortBy: string; - searchByText: string; + search: 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 + } \ No newline at end of file diff --git a/apps/api-gateway/src/issuance/issuance.controller.ts b/apps/api-gateway/src/issuance/issuance.controller.ts index 745c8716e..f640f0c64 100644 --- a/apps/api-gateway/src/issuance/issuance.controller.ts +++ b/apps/api-gateway/src/issuance/issuance.controller.ts @@ -1,3 +1,4 @@ +/* eslint-disable default-param-last */ /* eslint-disable no-param-reassign */ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable camelcase */ @@ -15,7 +16,9 @@ import { Header, UploadedFile, UseInterceptors, - Logger + Logger, + BadRequestException, + NotFoundException } from '@nestjs/common'; import { ApiTags, @@ -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'; @@ -85,6 +88,21 @@ export class IssuanceController { summary: `Get all issued credentials for a specific organization`, description: `Get all issued credentials for a specific organization` }) + @ApiQuery({ + name: 'pageNumber', + type: Number, + required: false + }) + @ApiQuery({ + name: 'pageSize', + type: Number, + required: false + }) + @ApiQuery({ + name: 'search', + type: String, + required: false + }) @ApiResponse({ status: HttpStatus.OK, description: 'Success', type: ApiResponseDto }) @ApiBearerAuth() @UseGuards(AuthGuard('jwt'), OrgRolesGuard) @@ -95,10 +113,10 @@ export class IssuanceController { @Param('orgId') orgId: string, @Res() res: Response ): Promise { - const { pageSize, searchByText, pageNumber, sortField, sortBy } = getAllIssuedCredentials; + const { pageSize, search, pageNumber, sortField, sortBy } = getAllIssuedCredentials; const issuedCredentialsSearchCriteria: IIssuedCredentialSearchParams = { pageNumber, - searchByText, + search, pageSize, sortField, sortBy @@ -220,7 +238,7 @@ export class IssuanceController { @Param('orgId') orgId: string, @Res() res: Response ): Promise { - try { + if (file) { const fileKey: string = uuidv4(); try { @@ -232,7 +250,7 @@ export class IssuanceController { const reqPayload: RequestPayload = { credDefId: credentialDefinitionId, fileKey, - fileName: fileDetails['fileName'].split('.csv')[0] + fileName: fileDetails['fileName'] || file?.filename || file?.originalname }; const importCsvDetails = await this.issueCredentialService.importCsv(reqPayload); @@ -243,9 +261,7 @@ export class IssuanceController { }; return res.status(HttpStatus.CREATED).json(finalResponse); } - } catch (error) { - throw new RpcException(error.response ? error.response : error); - } + } @Get('/orgs/:orgId/:requestId/preview') @@ -326,21 +342,59 @@ export class IssuanceController { summary: 'bulk issue credential', description: 'bulk issue credential' }) + @ApiConsumes('multipart/form-data') + @ApiBody({ + schema: { + type: 'object', + nullable: false, + required: ['file'], + properties: { + file: { + type: 'string', + format: 'binary' + } + } + }, + required: true + }) + @UseInterceptors(FileInterceptor('file')) + async issueBulkCredentials( @Param('requestId') requestId: string, @Param('orgId') orgId: string, @Res() res: Response, @Body() clientDetails: ClientDetails, - @User() user: user + @User() user: user, + @Query('credDefId') credentialDefinitionId?: string, + @Body() fileDetails?: object, + @UploadedFile() file?: Express.Multer.File ): Promise { + clientDetails.userId = user.id; - const bulkIssuanceDetails = await this.issueCredentialService.issueBulkCredential(requestId, orgId, clientDetails); - const finalResponse: IResponseType = { - statusCode: HttpStatus.CREATED, - message: ResponseMessages.issuance.success.bulkIssuance, - data: bulkIssuanceDetails.response - }; - return res.status(HttpStatus.CREATED).json(finalResponse); + let reqPayload: RequestPayload; + + if (file && clientDetails?.isSelectiveIssuance) { + const fileKey: string = uuidv4(); + try { + await this.awsService.uploadCsvFile(fileKey, file.buffer); + } catch (error) { + throw new RpcException(error.response ? error.response : error); + } + + reqPayload = { + credDefId: credentialDefinitionId, + fileKey, + fileName: fileDetails['fileName'] || file?.filename || file?.originalname + }; + } + const bulkIssuanceDetails = await this.issueCredentialService.issueBulkCredential(requestId, orgId, clientDetails, reqPayload); + + const finalResponse: IResponse = { + statusCode: HttpStatus.CREATED, + message: ResponseMessages.issuance.success.bulkIssuance, + data: bulkIssuanceDetails + }; + return res.status(HttpStatus.CREATED).json(finalResponse); } @Get('/orgs/:orgId/bulk/files') @@ -500,14 +554,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 { 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 @@ -532,15 +602,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 { issueCredentialDto.orgId = orgId; + issueCredentialDto.credentialType = credentialType; const getCredentialDetails = await this.issueCredentialService.sendCredentialOutOfBand(issueCredentialDto); const finalResponse: IResponseType = { statusCode: HttpStatus.CREATED, @@ -567,7 +643,6 @@ export class IssuanceController { @Res() res: Response ): Promise { issueCredentialDto.type = 'Issuance'; - this.logger.debug(`issueCredentialDto ::: ${JSON.stringify(issueCredentialDto)}`); const getCredentialDetails = await this.issueCredentialService.getIssueCredentialWebhook(issueCredentialDto, id).catch(error => { this.logger.debug(`error in saving issuance webhook ::: ${JSON.stringify(error)}`); diff --git a/apps/api-gateway/src/issuance/issuance.service.ts b/apps/api-gateway/src/issuance/issuance.service.ts index 11267f2f8..934cc80c0 100644 --- a/apps/api-gateway/src/issuance/issuance.service.ts +++ b/apps/api-gateway/src/issuance/issuance.service.ts @@ -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() @@ -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, isShortenUrl: issueCredentialDto.isShortenUrl, reuseConnection : issueCredentialDto.reuseConnection }; + } + 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, isShortenUrl: issueCredentialDto.isShortenUrl, reuseConnection : issueCredentialDto.reuseConnection }; + } + return this.sendNats(this.issuanceProxy, 'send-credential-create-offer-oob', payload); } @@ -108,9 +115,9 @@ export class IssuanceService extends BaseService { return this.sendNats(this.issuanceProxy, 'issued-file-data', payload); } - async issueBulkCredential(requestId: string, orgId: string, clientDetails: ClientDetails): Promise<{ response: object }> { - const payload = { requestId, orgId, clientDetails }; - return this.sendNats(this.issuanceProxy, 'issue-bulk-credentials', payload); + async issueBulkCredential(requestId: string, orgId: string, clientDetails: ClientDetails, reqPayload: RequestPayload): Promise { + const payload = { requestId, orgId, clientDetails, reqPayload }; + return this.sendNatsMessage(this.issuanceProxy, 'issue-bulk-credentials', payload); } async retryBulkCredential(fileId: string, orgId: string, clientId: string): Promise<{ response: object }> { diff --git a/apps/api-gateway/src/issuance/utils/helper.ts b/apps/api-gateway/src/issuance/utils/helper.ts new file mode 100644 index 000000000..574e5eb69 --- /dev/null +++ b/apps/api-gateway/src/issuance/utils/helper.ts @@ -0,0 +1,33 @@ +/* eslint-disable @typescript-eslint/array-type */ +import { ValidateBy, ValidationOptions, buildMessage, isString, isURL } from 'class-validator'; +import { JsonObject } from '../interfaces'; +export type SingleOrArray = T | T[] +export type JsonValue = string | number | boolean | null | JsonObject | JsonArray +export type JsonArray = Array + + +export const isJsonObject = (value: unknown): value is JsonObject => value !== undefined && 'object' === typeof value && null !== value && !Array.isArray(value); +export const CREDENTIALS_CONTEXT_V1_URL = 'https://www.w3.org/2018/credentials/v1'; +export function IsCredentialJsonLdContext(validationOptions?: ValidationOptions): PropertyDecorator { + return ValidateBy( + { + name: 'IsCredentialJsonLdContext', + validator: { + validate: (value): boolean => { + if (!Array.isArray(value)) { return false; } + + // First item must be the verifiable credential context + if (value[0] !== CREDENTIALS_CONTEXT_V1_URL) { return false; } + + return value.every((v) => (isString(v) && isURL(v)) || isJsonObject(v)); + }, + defaultMessage: buildMessage( + (eachPrefix) => `${eachPrefix + }$property must be an array of strings or objects, where the first item is the verifiable credential context URL.`, + validationOptions + ) + } + }, + validationOptions + ); +} \ No newline at end of file diff --git a/apps/api-gateway/src/main.ts b/apps/api-gateway/src/main.ts index 80d22c6ad..f8507b270 100644 --- a/apps/api-gateway/src/main.ts +++ b/apps/api-gateway/src/main.ts @@ -36,11 +36,11 @@ async function bootstrap(): Promise { .setDescription(`${process.env.PLATFORM_NAME} Platform APIs`) .setVersion('1.0') .addBearerAuth() - .addServer('http://localhost:5000') - .addServer('https://devapi.credebl.id') - .addServer('https://qa-api.credebl.id') - .addServer('https://api.credebl.id') - .addServer('https://sandboxapi.credebl.id') + .addServer(`${process.env.PUBLIC_DEV_API_URL}`) + .addServer(`${process.env.PUBLIC_LOCALHOST_URL}`) + .addServer(`${process.env.PUBLIC_QA_API_URL}`) + .addServer(`${process.env.PUBLIC_PRODUCTION_API_URL}`) + .addServer(`${process.env.PUBLIC_SANDBOX_API_URL}`) .addServer(`${process.env.API_GATEWAY_PROTOCOL}://${process.env.API_ENDPOINT}`) .addServer(`${process.env.API_GATEWAY_PROTOCOL}://${process.env.API_GATEWAY_HOST}`) .build(); diff --git a/apps/api-gateway/src/notification/dtos/notification.dto.ts b/apps/api-gateway/src/notification/dtos/notification.dto.ts new file mode 100644 index 000000000..8ddef46d3 --- /dev/null +++ b/apps/api-gateway/src/notification/dtos/notification.dto.ts @@ -0,0 +1,85 @@ +import { ApiExtraModels, ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString, IsUrl } from 'class-validator'; + +import { Transform } from 'class-transformer'; +import { trim } from '@credebl/common/cast.helper'; + +@ApiExtraModels() +export class RegisterHolderCredentalsDto { + + @ApiProperty() + @Transform(({ value }) => trim(value)) + @IsNotEmpty({ message: 'fcmToken is required.' }) + @IsString({ message: 'fcmToken must be in string format.' }) + fcmToken: string; + + @ApiProperty() + @Transform(({ value }) => trim(value)) + @IsNotEmpty({ message: 'orgId is required.' }) + @IsString({ message: 'orgId must be in string format.' }) + orgId: string; + + @ApiProperty() + @Transform(({ value }) => trim(value)) + @IsNotEmpty({ message: 'userKey is required.' }) + @IsString({ message: 'userKey must be in string format.' }) + userKey: string; +} + +export class RegisterOrgWebhhookEndpointDto { + + @ApiProperty() + @Transform(({ value }) => trim(value)) + @IsNotEmpty({ message: 'orgId is required.' }) + @IsString({ message: 'orgId must be in string format.' }) + orgId: string; + + @ApiProperty() + @Transform(({ value }) => trim(value)) + @IsNotEmpty({ message: 'notificationWebhook is required.' }) + @IsString({ message: 'notificationWebhook must be in string format.' }) + @IsUrl({ + // eslint-disable-next-line camelcase + require_protocol: true, // require URL protocol (e.g., http:// or https://) + // eslint-disable-next-line camelcase + require_tld: true // require top-level domain (e.g., .com, .net) + + }) + notificationWebhook: string; +} + +export class SendNotificationDto { + + @ApiProperty() + @Transform(({ value }) => trim(value)) + @IsNotEmpty({ message: 'fcmToken is required.' }) + @IsString({ message: 'fcmToken must be in string format.' }) + fcmToken: string; + + @ApiProperty() + @Transform(({ value }) => trim(value)) + @IsNotEmpty({ message: 'messageType is required.' }) + @IsString({ message: 'messageType must be in string format.' }) + messageType: string; + + @ApiProperty() + @Transform(({ value }) => trim(value)) + @IsNotEmpty({ message: 'clientCode is required.' }) + @IsString({ message: 'clientCode must be in string format.' }) + clientCode: string; +} + +export class GetNotificationDto { + + @ApiProperty() + @Transform(({ value }) => trim(value)) + @IsNotEmpty({ message: 'fcmToken is required.' }) + @IsString({ message: 'fcmToken must be in string format.' }) + fcmToken: string; + + @ApiProperty() + @Transform(({ value }) => trim(value)) + @IsNotEmpty({ message: 'messageType is required.' }) + @IsString({ message: 'messageType must be in string format.' }) + messageType: string; +} \ No newline at end of file diff --git a/apps/api-gateway/src/notification/interfaces/notification.interfaces.ts b/apps/api-gateway/src/notification/interfaces/notification.interfaces.ts new file mode 100644 index 000000000..a2b4aa1a3 --- /dev/null +++ b/apps/api-gateway/src/notification/interfaces/notification.interfaces.ts @@ -0,0 +1,5 @@ +export interface INotification { + id: string; + orgId: string; + webhookEndpoint: string; +} \ No newline at end of file diff --git a/apps/api-gateway/src/notification/notification.controller.ts b/apps/api-gateway/src/notification/notification.controller.ts new file mode 100644 index 000000000..22c82fc24 --- /dev/null +++ b/apps/api-gateway/src/notification/notification.controller.ts @@ -0,0 +1,82 @@ +import { CustomExceptionFilter } from '@credebl/common/exception-handler'; +import { Body, Controller, HttpStatus, Logger, Post, Res, UseFilters } from '@nestjs/common'; +import { ApiForbiddenResponse, ApiOperation, ApiResponse, ApiTags, ApiUnauthorizedResponse } from '@nestjs/swagger'; +import { UnauthorizedErrorDto } from '../dtos/unauthorized-error.dto'; +import { ForbiddenErrorDto } from '../dtos/forbidden-error.dto'; +import { ApiResponseDto } from '../dtos/apiResponse.dto'; +import { RegisterOrgWebhhookEndpointDto, SendNotificationDto } from './dtos/notification.dto'; +import { IResponse } from '@credebl/common/interfaces/response.interface'; +import { Response } from 'express'; +import { ResponseMessages } from '@credebl/common/response-messages'; +import { NotificationService } from './notification.service'; + + +@Controller('notification') +@UseFilters(CustomExceptionFilter) +@ApiTags('notification') +@ApiUnauthorizedResponse({ status: 401, description: 'Unauthorized', type: UnauthorizedErrorDto }) +@ApiForbiddenResponse({ status: 403, description: 'Forbidden', type: ForbiddenErrorDto }) +export class NotificationController { + constructor( + private readonly notificationService: NotificationService + ) { } + private readonly logger = new Logger('NotificationController'); + + /** + * Register organization webhook endpoint + * @param registerOrgWebhhookEndpointDto + * @param res + * @returns Stored notification data + */ + @Post('/register/webhook-endpoint') + @ApiOperation({ + summary: `Register organization webhook endpoint for notification`, + description: `Register organization webhook endpoint for notification` + }) + @ApiResponse({ status: HttpStatus.CREATED, description: 'Success', type: ApiResponseDto }) + async registerOrgWebhookEndpoint( + @Body() registerOrgWebhhookEndpointDto: RegisterOrgWebhhookEndpointDto, + @Res() res: Response + ): Promise { + + const registerUserEndpoint = await this.notificationService.registerOrgWebhookEndpoint( + registerOrgWebhhookEndpointDto + ); + + const finalResponse: IResponse = { + statusCode: HttpStatus.CREATED, + message: ResponseMessages.notification.success.register, + data: registerUserEndpoint + }; + return res.status(HttpStatus.CREATED).json(finalResponse); + } + + /** + * Send notification for holder + * @param sendNotificationDto + * @param res + * @returns Get notification details + */ + @Post('/') + @ApiOperation({ + summary: `Send notification for holder`, + description: `Send notification for holder` + }) + @ApiResponse({ status: HttpStatus.CREATED, description: 'Success', type: ApiResponseDto }) + async sendNotification( + @Body() notificationRequestBody: SendNotificationDto, + @Res() res: Response + ): Promise { + + const sendNotification = await this.notificationService.sendNotification( + notificationRequestBody + ); + + const finalResponse: IResponse = { + statusCode: HttpStatus.CREATED, + message: ResponseMessages.notification.success.sendNotification, + data: sendNotification + }; + return res.status(HttpStatus.CREATED).json(finalResponse); + } +} \ No newline at end of file diff --git a/apps/api-gateway/src/notification/notification.module.ts b/apps/api-gateway/src/notification/notification.module.ts new file mode 100644 index 000000000..bc3bc7706 --- /dev/null +++ b/apps/api-gateway/src/notification/notification.module.ts @@ -0,0 +1,28 @@ + +import { CommonModule, CommonService } from '@credebl/common'; +import { ClientsModule, Transport } from '@nestjs/microservices'; +import { ConfigModule } from '@nestjs/config'; +import { HttpModule } from '@nestjs/axios'; +import { Module } from '@nestjs/common'; +import { getNatsOptions } from '@credebl/common/nats.config'; +import { NotificationController } from './notification.controller'; +import { NotificationService } from './notification.service'; + +@Module({ + imports: [ + HttpModule, + ConfigModule.forRoot(), + ClientsModule.register([ + { + name: 'NATS_CLIENT', + transport: Transport.NATS, + options: getNatsOptions(process.env.API_GATEWAY_NKEY_SEED) + + }, + CommonModule + ]) + ], + controllers: [NotificationController], + providers: [NotificationService, CommonService] +}) +export class NotificationModule { } diff --git a/apps/api-gateway/src/notification/notification.service.ts b/apps/api-gateway/src/notification/notification.service.ts new file mode 100644 index 000000000..01847e1d9 --- /dev/null +++ b/apps/api-gateway/src/notification/notification.service.ts @@ -0,0 +1,30 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { ClientProxy } from '@nestjs/microservices'; +import { BaseService } from 'libs/service/base.service'; +import { RegisterOrgWebhhookEndpointDto, SendNotificationDto } from './dtos/notification.dto'; +import { INotification } from './interfaces/notification.interfaces'; + +@Injectable() +export class NotificationService extends BaseService { + constructor(@Inject('NATS_CLIENT') private readonly serviceProxy: ClientProxy) { + super('NotificationService'); + } + + /** + * Register organization webhook endpoint + * @param registerOrgWebhhookEndpointDto + * @returns Stored notification data + */ + async registerOrgWebhookEndpoint(registerOrgWebhhookEndpointDto: RegisterOrgWebhhookEndpointDto): Promise { + return this.sendNatsMessage(this.serviceProxy, 'register-org-webhook-endpoint-for-notification', registerOrgWebhhookEndpointDto); + } + + /** + * Send notification for holder + * @param sendNotificationDto + * @returns Get notification details + */ + async sendNotification(notificationRequestBody: SendNotificationDto): Promise { + return this.sendNatsMessage(this.serviceProxy, 'send-notification', notificationRequestBody); + } +} \ No newline at end of file diff --git a/apps/api-gateway/src/organization/dtos/create-organization-dto.ts b/apps/api-gateway/src/organization/dtos/create-organization-dto.ts index 6847b57ba..61e2c0f73 100644 --- a/apps/api-gateway/src/organization/dtos/create-organization-dto.ts +++ b/apps/api-gateway/src/organization/dtos/create-organization-dto.ts @@ -1,5 +1,5 @@ import { ApiExtraModels, ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { IsNotEmpty, IsOptional, IsString, MaxLength, MinLength } from 'class-validator'; +import { IsNotEmpty, IsOptional, IsString, IsUrl, MaxLength, MinLength } from 'class-validator'; import { Transform } from 'class-transformer'; import { trim } from '@credebl/common/cast.helper'; @@ -28,6 +28,20 @@ export class CreateOrganizationDto { @Transform(({ value }) => trim(value)) website?: string; + @ApiPropertyOptional() + @IsOptional() + @Transform(({ value }) => trim(value)) + @IsNotEmpty({ message: 'notificationWebhook is required.' }) + @IsString({ message: 'notificationWebhook must be in string format.' }) + @IsUrl({ + // eslint-disable-next-line camelcase + require_protocol: true, // require URL protocol (e.g., http:// or https://) + // eslint-disable-next-line camelcase + require_tld: true // require top-level domain (e.g., .com, .net) + + }) + notificationWebhook?: string; + @ApiPropertyOptional() @IsOptional() @Transform(({ value }) => trim(value)) diff --git a/apps/api-gateway/src/organization/dtos/send-invitation.dto.ts b/apps/api-gateway/src/organization/dtos/send-invitation.dto.ts index 013cb70f7..456215420 100644 --- a/apps/api-gateway/src/organization/dtos/send-invitation.dto.ts +++ b/apps/api-gateway/src/organization/dtos/send-invitation.dto.ts @@ -37,8 +37,5 @@ export class BulkSendInvitationDto { @Type(() => SendInvitationDto) invitations: SendInvitationDto[]; - @ApiProperty() - @IsString({ message: 'orgId should be a string' }) - @IsNotEmpty({ message: 'orgId is required' }) orgId: string; } diff --git a/apps/api-gateway/src/organization/organization.controller.ts b/apps/api-gateway/src/organization/organization.controller.ts index 20635a1f9..51bb113a0 100644 --- a/apps/api-gateway/src/organization/organization.controller.ts +++ b/apps/api-gateway/src/organization/organization.controller.ts @@ -24,6 +24,7 @@ import { ImageServiceService } from '@credebl/image-service'; import { ClientCredentialsDto } from './dtos/client-credentials.dto'; import { PaginationDto } from '@credebl/common/dtos/pagination.dto'; import { validate as isValidUUID } from 'uuid'; +import { UserAccessGuard } from '../authz/guards/user-access-guard'; @UseFilters(CustomExceptionFilter) @Controller('orgs') @@ -95,7 +96,7 @@ export class OrganizationController { * @returns get organization roles */ - @Get('/roles') + @Get('/:orgId/roles') @ApiOperation({ summary: 'Fetch org-roles details', description: 'Fetch org-roles details' @@ -103,9 +104,9 @@ export class OrganizationController { @ApiResponse({ status: HttpStatus.OK, description: 'Success', type: ApiResponseDto }) @UseGuards(AuthGuard('jwt')) @ApiBearerAuth() - async getOrgRoles(@Res() res: Response): Promise { + async getOrgRoles(@Param('orgId', new ParseUUIDPipe({exceptionFactory: (): Error => { throw new BadRequestException(ResponseMessages.organisation.error.invalidOrgId); }})) orgId: string, @Res() res: Response): Promise { - const orgRoles = await this.organizationService.getOrgRoles(); + const orgRoles = await this.organizationService.getOrgRoles(orgId.trim()); const finalResponse: IResponse = { statusCode: HttpStatus.OK, @@ -211,7 +212,7 @@ export class OrganizationController { @Get('/') @ApiOperation({ summary: 'Get all organizations', description: 'Get all organizations' }) @ApiResponse({ status: HttpStatus.OK, description: 'Success', type: ApiResponseDto }) - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuard('jwt'), UserAccessGuard) @ApiBearerAuth() @ApiQuery({ name: 'pageNumber', @@ -324,10 +325,14 @@ export class OrganizationController { @Post('/') @ApiOperation({ summary: 'Create a new Organization', description: 'Create an organization' }) @ApiResponse({ status: HttpStatus.CREATED, description: 'Success', type: ApiResponseDto }) - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuard('jwt'), UserAccessGuard) @ApiBearerAuth() async createOrganization(@Body() createOrgDto: CreateOrganizationDto, @Res() res: Response, @User() reqUser: user): Promise { - const orgData = await this.organizationService.createOrganization(createOrgDto, reqUser.id); + + // eslint-disable-next-line prefer-destructuring + const keycloakUserId = reqUser.keycloakUserId; + + const orgData = await this.organizationService.createOrganization(createOrgDto, reqUser.id, keycloakUserId); const finalResponse: IResponse = { statusCode: HttpStatus.CREATED, message: ResponseMessages.organisation.success.create, @@ -347,10 +352,14 @@ export class OrganizationController { @Roles(OrgRoles.OWNER) @ApiOperation({ summary: 'Create credentials for an organization', description: 'Create client id and secret for an organization' }) @ApiResponse({ status: HttpStatus.CREATED, description: 'Success', type: ApiResponseDto }) - @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard, UserAccessGuard) @ApiBearerAuth() async createOrgCredentials(@Param('orgId') orgId: string, @Res() res: Response, @User() reqUser: user): Promise { - const orgCredentials = await this.organizationService.createOrgCredentials(orgId, reqUser.id); + + // eslint-disable-next-line prefer-destructuring + const keycloakUserId = reqUser.keycloakUserId; + + const orgCredentials = await this.organizationService.createOrgCredentials(orgId, reqUser.id, keycloakUserId); const finalResponse: IResponse = { statusCode: HttpStatus.CREATED, message: ResponseMessages.organisation.success.orgCredentials, @@ -381,10 +390,32 @@ export class OrganizationController { return res.status(HttpStatus.OK).json(finalResponse); } + @Post('/register-org-map-users') + @ApiOperation({ + summary: 'Register client and map users', + description: 'Register client and map users' + }) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + @Roles(OrgRoles.PLATFORM_ADMIN) + @ApiBearerAuth() + @ApiResponse({ status: HttpStatus.OK, description: 'Success', type: ApiResponseDto }) + async registerOrgsMapUsers(@Res() res: Response): Promise { + + await this.organizationService.registerOrgsMapUsers(); + + const finalResponse: IResponse = { + statusCode: HttpStatus.CREATED, + message: 'Organization client created and users mapped to client' + }; + + return res.status(HttpStatus.CREATED).json(finalResponse); + + } + @Post('/:orgId/invitations') @ApiOperation({ summary: 'Create organization invitation', - description: 'Create send invitation' + description: 'Create organization invitation' }) @ApiResponse({ status: HttpStatus.OK, description: 'Success', type: ApiResponseDto }) @Roles(OrgRoles.OWNER, OrgRoles.SUPER_ADMIN, OrgRoles.ADMIN) @@ -441,7 +472,7 @@ export class OrganizationController { @ApiResponse({ status: HttpStatus.OK, description: 'Success', type: ApiResponseDto }) @ApiBearerAuth() @Roles(OrgRoles.OWNER, OrgRoles.ADMIN) - @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard, UserAccessGuard) async updateOrganization(@Body() updateOrgDto: UpdateOrganizationDto, @Param('orgId') orgId: string, @Res() res: Response, @User() reqUser: user): Promise { updateOrgDto.orgId = orgId; @@ -482,6 +513,7 @@ export class OrganizationController { @ApiOperation({ summary: 'Delete Organization Client Credentials', description: 'Delete Organization Client Credentials' }) @ApiResponse({ status: HttpStatus.OK, description: 'Success', type: ApiResponseDto }) @ApiBearerAuth() + @ApiExcludeEndpoint() @UseGuards(AuthGuard('jwt')) async deleteOrgClientCredentials(@Param('orgId') orgId: string, @Res() res: Response): Promise { diff --git a/apps/api-gateway/src/organization/organization.service.ts b/apps/api-gateway/src/organization/organization.service.ts index 1e4d377d8..0baf88fa0 100644 --- a/apps/api-gateway/src/organization/organization.service.ts +++ b/apps/api-gateway/src/organization/organization.service.ts @@ -6,7 +6,6 @@ import { CreateOrganizationDto } from './dtos/create-organization-dto'; import { BulkSendInvitationDto } from './dtos/send-invitation.dto'; import { UpdateUserRolesDto } from './dtos/update-user-roles.dto'; import { UpdateOrganizationDto } from './dtos/update-organization-dto'; -import { IOrgRoles } from 'libs/org-roles/interfaces/org-roles.interface'; import { organisation } from '@prisma/client'; import { IGetOrgById, IGetOrganization } from 'apps/organization/interfaces/organization.interface'; import { IOrgUsers } from 'apps/user/interfaces/user.interface'; @@ -14,6 +13,7 @@ import { IOrgCredentials, IOrganization, IOrganizationInvitations, IOrganization import { ClientCredentialsDto } from './dtos/client-credentials.dto'; import { IAccessTokenData } from '@credebl/common/interfaces/interface'; import { PaginationDto } from '@credebl/common/dtos/pagination.dto'; +import { IClientRoles } from '@credebl/client-registration/interfaces/client.interface'; @Injectable() export class OrganizationService extends BaseService { @@ -26,8 +26,8 @@ export class OrganizationService extends BaseService { * @param createOrgDto * @returns Organization creation Success */ - async createOrganization(createOrgDto: CreateOrganizationDto, userId: string): Promise { - const payload = { createOrgDto, userId }; + async createOrganization(createOrgDto: CreateOrganizationDto, userId: string, keycloakUserId: string): Promise { + const payload = { createOrgDto, userId, keycloakUserId }; return this.sendNatsMessage(this.serviceProxy, 'create-organization', payload); } @@ -37,8 +37,8 @@ export class OrganizationService extends BaseService { * @param userId * @returns Orgnization client credentials */ - async createOrgCredentials(orgId: string, userId: string): Promise { - const payload = { orgId, userId }; + async createOrgCredentials(orgId: string, userId: string, keycloakUserId: string): Promise { + const payload = { orgId, userId, keycloakUserId }; return this.sendNatsMessage(this.serviceProxy, 'create-org-credentials', payload); } @@ -133,8 +133,8 @@ export class OrganizationService extends BaseService { * @returns get organization roles */ - async getOrgRoles(): Promise { - const payload = {}; + async getOrgRoles(orgId: string): Promise { + const payload = {orgId}; return this.sendNatsMessage(this.serviceProxy, 'get-org-roles', payload); } @@ -148,6 +148,11 @@ export class OrganizationService extends BaseService { return this.sendNatsMessage(this.serviceProxy, 'send-invitation', payload); } + async registerOrgsMapUsers(): Promise { + const payload = {}; + return this.sendNatsMessage(this.serviceProxy, 'register-orgs-users-map', payload); + } + /** * * @param updateUserDto diff --git a/apps/api-gateway/src/schema/interfaces/index.ts b/apps/api-gateway/src/schema/interfaces/index.ts index a0edf7816..ace0f7e67 100644 --- a/apps/api-gateway/src/schema/interfaces/index.ts +++ b/apps/api-gateway/src/schema/interfaces/index.ts @@ -1,6 +1,7 @@ import { UserRoleOrgPermsDto } from '../../dtos/user-role-org-perms.dto'; export interface IUserRequestInterface { + id: string; userId: string; email: string; orgId: string; diff --git a/apps/api-gateway/src/schema/schema.controller.ts b/apps/api-gateway/src/schema/schema.controller.ts index 596be7c4d..15335fa03 100644 --- a/apps/api-gateway/src/schema/schema.controller.ts +++ b/apps/api-gateway/src/schema/schema.controller.ts @@ -17,7 +17,7 @@ import { OrgRoles } from 'libs/org-roles/enums'; import { Roles } from '../authz/decorators/roles.decorator'; import { IUserRequestInterface } from './interfaces'; import { OrgRolesGuard } from '../authz/guards/org-roles.guard'; -import { CreateSchemaDto } from '../dtos/create-schema.dto'; +import { CreateSchemaDto, CreateW3CSchemaDto } from '../dtos/create-schema.dto'; import { CustomExceptionFilter } from 'apps/api-gateway/common/exception-handler'; import { CredDefSortFields, SortFields } from 'apps/ledger/src/schema/enum/schema.enum'; @@ -133,6 +133,26 @@ export class SchemaController { return res.status(HttpStatus.OK).json(finalResponse); } + @Post('/:orgId/polygon-w3c/schemas') + @ApiOperation({ + summary: 'Create and sends a W3C-schema to the ledger.', + description: 'Create and sends a W3C-schema to the ledger.' + }) + @Roles(OrgRoles.OWNER, OrgRoles.ADMIN, OrgRoles.ISSUER, OrgRoles.VERIFIER, OrgRoles.MEMBER) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + @ApiResponse({ status: HttpStatus.CREATED, description: 'Success', type: ApiResponseDto }) + async createW3CSchema(@Res() res: Response, @Body() schemaPayload: CreateW3CSchemaDto, @Param('orgId') orgId: string, @User() user: IUserRequestInterface): Promise { + + const schemaDetails = await this.appService.createW3CSchema(schemaPayload, orgId); + + const finalResponse: IResponseType = { + statusCode: HttpStatus.CREATED, + message: ResponseMessages.schema.success.create, + data: schemaDetails + }; + return res.status(HttpStatus.CREATED).json(finalResponse); + } + @Post('/:orgId/schemas') @ApiOperation({ summary: 'Create and sends a schema to the ledger.', diff --git a/apps/api-gateway/src/schema/schema.service.ts b/apps/api-gateway/src/schema/schema.service.ts index a1d0863a3..f0655d307 100644 --- a/apps/api-gateway/src/schema/schema.service.ts +++ b/apps/api-gateway/src/schema/schema.service.ts @@ -2,7 +2,7 @@ import { Injectable, Inject } from '@nestjs/common'; import { ClientProxy } from '@nestjs/microservices'; import { BaseService } from '../../../../libs/service/base.service'; import { CreateSchemaDto } from '../dtos/create-schema.dto'; -import { ISchemaSearchPayload } from '../interfaces/ISchemaSearch.interface'; +import { ISchemaSearchPayload, W3CSchemaPayload } from '../interfaces/ISchemaSearch.interface'; import { IUserRequestInterface } from './interfaces'; import { ICredDefWithPagination, ISchemaData, ISchemasWithPagination } from '@credebl/common/interfaces/schema.interface'; import { GetCredentialDefinitionBySchemaIdDto } from './dtos/get-all-schema.dto'; @@ -19,6 +19,11 @@ export class SchemaService extends BaseService { return this.sendNatsMessage(this.schemaServiceProxy, 'create-schema', payload); } + createW3CSchema(schemaPayload: W3CSchemaPayload, orgId: string): Promise { + const payload = { schemaPayload, orgId }; + return this.sendNatsMessage(this.schemaServiceProxy, 'create-w3c-schema', payload); + } + getSchemaById(schemaId: string, orgId: string): Promise<{ response: object; }> { diff --git a/apps/api-gateway/src/user/dto/update-platform-settings.dto.ts b/apps/api-gateway/src/user/dto/update-platform-settings.dto.ts index 79d4cdb76..d452e213a 100644 --- a/apps/api-gateway/src/user/dto/update-platform-settings.dto.ts +++ b/apps/api-gateway/src/user/dto/update-platform-settings.dto.ts @@ -23,7 +23,7 @@ export class UpdatePlatformSettingsDto { @IsString({ message: 'emailFrom should be string' }) emailFrom: string; - @ApiProperty({ example: 'dev.credebl.id' }) + @ApiProperty({ example: `${process.env.UPLOAD_LOGO_HOST}` }) @IsOptional() @IsString({ message: 'API endpoint should be string' }) apiEndPoint: string; diff --git a/apps/api-gateway/src/user/user.controller.ts b/apps/api-gateway/src/user/user.controller.ts index 8e9ee72a4..5e85bbfbc 100644 --- a/apps/api-gateway/src/user/user.controller.ts +++ b/apps/api-gateway/src/user/user.controller.ts @@ -50,6 +50,7 @@ import { OrgRoles } from 'libs/org-roles/enums'; import { AwsService } from '@credebl/aws/aws.service'; import { PaginationDto } from '@credebl/common/dtos/pagination.dto'; import { CreateCertificateDto } from './dto/share-certificate.dto'; +import { UserAccessGuard } from '../authz/guards/user-access-guard'; @UseFilters(CustomExceptionFilter) @Controller('users') @@ -132,7 +133,7 @@ export class UserController { summary: 'Fetch login user details', description: 'Fetch login user details' }) - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuard('jwt'), UserAccessGuard) @ApiBearerAuth() async getProfile(@User() reqUser: user, @Res() res: Response): Promise { const userData = await this.userService.getProfile(reqUser.id); @@ -154,7 +155,7 @@ export class UserController { summary: 'Get all platform and ecosystem settings', description: 'Get all platform and ecosystem settings' }) - @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard, UserAccessGuard) @Roles(OrgRoles.PLATFORM_ADMIN) @ApiBearerAuth() async getPlatformSettings(@Res() res: Response): Promise { @@ -174,7 +175,7 @@ export class UserController { summary: 'users activity', description: 'Fetch users activity' }) - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuard('jwt'), UserAccessGuard) @ApiBearerAuth() @ApiQuery({ name: 'limit', required: true }) async getUserActivities( @@ -201,7 +202,7 @@ export class UserController { summary: 'organization invitations', description: 'Fetch organization invitations' }) - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuard('jwt'), UserAccessGuard) @ApiBearerAuth() @ApiQuery({ name: 'pageNumber', @@ -293,7 +294,7 @@ export class UserController { summary: 'accept/reject organization invitation', description: 'Accept or Reject organization invitations' }) - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuard('jwt'), UserAccessGuard) @ApiBearerAuth() async acceptRejectInvitaion( @Body() acceptRejectInvitation: AcceptRejectInvitationDto, @@ -349,7 +350,7 @@ export class UserController { }) @ApiResponse({ status: HttpStatus.OK, description: 'Success', type: ApiResponseDto }) @ApiBearerAuth() - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuard('jwt'), UserAccessGuard) async updateUserProfile( @Body() updateUserProfileDto: UpdateUserProfileDto, @User() reqUser: user, @@ -375,7 +376,7 @@ export class UserController { @ApiOperation({ summary: 'Store user password details', description: 'Store user password details' }) @ApiExcludeEndpoint() @ApiBearerAuth() - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuard('jwt'), UserAccessGuard) async addPasskey( @Body() userInfo: AddPasskeyDetailsDto, @@ -403,7 +404,7 @@ export class UserController { summary: 'Update platform and ecosystem settings', description: 'Update platform and ecosystem settings' }) - @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard, UserAccessGuard) @Roles(OrgRoles.PLATFORM_ADMIN) @ApiBearerAuth() async updatePlatformSettings( diff --git a/apps/api-gateway/src/utilities/dtos/shortening-url.dto.ts b/apps/api-gateway/src/utilities/dtos/shortening-url.dto.ts index 287778e40..f1467028b 100644 --- a/apps/api-gateway/src/utilities/dtos/shortening-url.dto.ts +++ b/apps/api-gateway/src/utilities/dtos/shortening-url.dto.ts @@ -39,3 +39,9 @@ interface Attribute { name: string; value: string; } + + export class GenericDto { + @ApiProperty() + @IsNotEmpty() + data: string | object; +} \ No newline at end of file diff --git a/apps/api-gateway/src/utilities/utilities.controller.ts b/apps/api-gateway/src/utilities/utilities.controller.ts index 35984c216..782b90c6e 100644 --- a/apps/api-gateway/src/utilities/utilities.controller.ts +++ b/apps/api-gateway/src/utilities/utilities.controller.ts @@ -1,5 +1,5 @@ import { ApiForbiddenResponse, ApiOperation, ApiResponse, ApiTags, ApiUnauthorizedResponse } from '@nestjs/swagger'; -import { Controller, UseFilters, Post, Body, Res, HttpStatus } from '@nestjs/common'; +import { Controller, UseFilters, Post, Body, Res, HttpStatus, Param } from '@nestjs/common'; import IResponse from '@credebl/common/interfaces/response.interface'; import { Response } from 'express'; import { ApiResponseDto } from '../dtos/apiResponse.dto'; @@ -7,7 +7,7 @@ import { UnauthorizedErrorDto } from '../dtos/unauthorized-error.dto'; import { ForbiddenErrorDto } from '../dtos/forbidden-error.dto'; import { ResponseMessages } from '@credebl/common/response-messages'; import { CustomExceptionFilter } from 'apps/api-gateway/common/exception-handler'; -import { UtilitiesDto } from './dtos/shortening-url.dto'; +import { GenericDto, UtilitiesDto } from './dtos/shortening-url.dto'; import { UtilitiesService } from './utilities.service'; @UseFilters(CustomExceptionFilter) @@ -20,7 +20,7 @@ export class UtilitiesController { constructor( private readonly utilitiesService: UtilitiesService ) { } - + @Post('/') @ApiOperation({ summary: 'Create a shorteningurl', description: 'Create a shortening url' }) @@ -35,5 +35,18 @@ export class UtilitiesController { return res.status(HttpStatus.CREATED).json(finalResponse); } + @Post('/store-object/:persist') + @ApiOperation({ summary: 'Store an object and return a short url to it', description: 'Create a short url representing the object' }) + @ApiResponse({ status: HttpStatus.CREATED, description: 'Created', type: ApiResponseDto }) + async storeObject(@Body() storeObjectDto: GenericDto, @Param('persist') persist: boolean, @Res() res: Response): Promise { + const shorteningUrl = await this.utilitiesService.storeObject(persist.valueOf(), storeObjectDto); + const finalResponse: IResponse = { + statusCode: HttpStatus.CREATED, + message: ResponseMessages.storeObject.success.storeObject, + data: shorteningUrl + }; + return res.status(HttpStatus.CREATED).json(finalResponse); + } + } diff --git a/apps/api-gateway/src/utilities/utilities.service.ts b/apps/api-gateway/src/utilities/utilities.service.ts index fae204616..08aa46f5f 100644 --- a/apps/api-gateway/src/utilities/utilities.service.ts +++ b/apps/api-gateway/src/utilities/utilities.service.ts @@ -12,4 +12,9 @@ export class UtilitiesService extends BaseService { async createShorteningUrl(shorteningUrlDto: UtilitiesDto): Promise { return this.sendNatsMessage(this.serviceProxy, 'create-shortening-url', shorteningUrlDto); } + + async storeObject(persistent: boolean, storeObj: unknown): Promise { + const payload = {persistent, storeObj}; + return this.sendNatsMessage(this.serviceProxy, 'store-object-return-url', payload); + } } diff --git a/apps/api-gateway/src/verification/dto/request-proof.dto.ts b/apps/api-gateway/src/verification/dto/request-proof.dto.ts index 9ca102123..e74db5161 100644 --- a/apps/api-gateway/src/verification/dto/request-proof.dto.ts +++ b/apps/api-gateway/src/verification/dto/request-proof.dto.ts @@ -1,28 +1,42 @@ -import { IsArray, IsBoolean, IsEmail, IsEnum, IsNotEmpty, IsNumberString, IsObject, IsOptional, IsString } from 'class-validator'; +import { ArrayNotEmpty, IsArray, IsBoolean, IsEmail, IsEnum, IsNotEmpty, IsNumberString, IsObject, IsOptional, IsString, ValidateIf, ValidateNested, IsUUID, ArrayUnique, ArrayMaxSize } from 'class-validator'; import { toLowerCase, trim } from '@credebl/common/cast.helper'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { Transform } from 'class-transformer'; +import { Transform, Type } from 'class-transformer'; import { AutoAccept } from '@credebl/enum/enum'; +import { IProofFormats } from '../interfaces/verification.interface'; export class ProofRequestAttribute { + + @ValidateIf((obj) => obj.attributeNames === undefined) + @IsNotEmpty() @IsString() - @IsNotEmpty({ message: 'attributeName is required.' }) - attributeName: string; + attributeName?: string; + + @ValidateIf((obj) => obj.attributeName === undefined) + @IsArray({ message: 'attributeNames must be an array.' }) + @ArrayNotEmpty({ message: 'array can not be empty' }) + @IsString({ each: true }) + @IsNotEmpty({ each: true, message: 'each element cannot be empty' }) + attributeNames?: string[]; + @ApiPropertyOptional() @IsString() @IsOptional() schemaId?: string; + @ApiPropertyOptional() @IsString() @IsOptional() @IsNotEmpty({ message: 'condition is required.' }) condition?: string; + @ApiPropertyOptional() @IsOptional() @IsNotEmpty({ message: 'value is required.' }) @IsNumberString({}, { message: 'Value must be a number' }) value?: string; + @ApiPropertyOptional() @IsString() @IsOptional() credDefId?: string; @@ -71,11 +85,14 @@ export class RequestProofDto extends ProofPayload { credDefId: 'string', schemaId: 'string' } - ] + ], + type: () => [ProofRequestAttribute] }) @IsArray({ message: 'attributes must be in array' }) + @ValidateNested() @IsObject({ each: true }) @IsNotEmpty({ message: 'please provide valid attributes' }) + @Type(() => ProofRequestAttribute) attributes: ProofRequestAttribute[]; @ApiPropertyOptional() @@ -105,11 +122,14 @@ export class OutOfBandRequestProof extends ProofPayload { credDefId: '', schemaId: '' } - ] + ], + type: () => [ProofRequestAttribute] }) @IsArray({ message: 'attributes must be in array' }) + @ValidateNested({ each: true }) @IsObject({ each: true }) @IsNotEmpty({ message: 'please provide valid attributes' }) + @Type(() => ProofRequestAttribute) attributes: ProofRequestAttribute[]; @ApiProperty() @@ -134,3 +154,204 @@ export class OutOfBandRequestProof extends ProofPayload { }) autoAcceptProof: string; } + +export class Fields { + @ApiProperty() + @IsArray() + @IsNotEmpty({ message: 'path is required.' }) + path: string[]; + } + +export class Constraints { + @ApiProperty({type: () => [Fields]}) + @IsOptional() + @IsNotEmpty({ message: 'Fields are required.' }) + @ValidateNested() + @Type(() => Fields) + fields: Fields[]; + } + + +export class Schema { + @ApiProperty() + @IsNotEmpty({ message: 'uri is required.' }) + @IsString() + uri:string; + +} +export class InputDescriptors { + @ApiProperty() + @IsNotEmpty({ message: 'id is required.' }) + @IsString() + id:string; + + @ApiProperty() + @IsString() + @IsOptional() + @IsNotEmpty({ message: 'name is required.' }) + name:string; + + @ApiProperty() + @IsString() + @IsOptional() + @IsNotEmpty({ message: 'purpose is required.' }) + purpose:string; + + + @ApiProperty({type: () => [Schema]}) + @IsNotEmpty({ message: 'schema is required.' }) + @ValidateNested() + @Type(() => Schema) + schema:Schema[]; + + + @ApiProperty({type: () => Constraints}) + @IsOptional() + @IsNotEmpty({ message: 'Constraints are required.' }) + @ValidateNested() + @Type(() => Constraints) + constraints:Constraints; + +} + +export class ProofRequestPresentationDefinition { + + @IsString() + @IsNotEmpty({ message: 'id is required.' }) + id: string; + @ApiProperty({type: () => [InputDescriptors]}) + @IsNotEmpty({ message: 'inputDescriptors is required.' }) + @IsArray({ message: 'inputDescriptors must be an array' }) + @IsObject({ each: true }) + @Type(() => InputDescriptors) + @ValidateNested() + + // eslint-disable-next-line camelcase + input_descriptors:InputDescriptors[]; +} + +export class SendProofRequestPayload { + + @ApiPropertyOptional() + @IsOptional() + @IsNotEmpty({ message: 'Please provide valid goal code' }) + @IsString({ message: 'goal code should be string' }) + goalCode?: string; + + @ApiPropertyOptional() + @IsString({ message: 'protocolVersion must be in string' }) + @IsNotEmpty({ message: 'please provide valid protocol version' }) + @IsOptional() + protocolVersion: string; + + @ApiPropertyOptional() + @IsOptional() + @IsString({ message: 'comment must be in string' }) + comment: string; + + @ApiProperty({ + 'example': [ + { + indy: { + name: 'Verify national identity', + version: '1.0', + // eslint-disable-next-line camelcase + requested_attributes: { + verifynameAddress: { + names: ['name', 'address'], + restrictions: [{ 'schema_id': 'KU583UbI4yAKfaBTSz1rqG:2:National ID:1.0.0' }] + }, + verifyBirthPlace: { + name: 'Place', + restrictions: [{ 'schema_id': 'KU583UbI4yAKfaBTSz1rqG:2:Birth Certificate:1.0.0' }] + } + }, + // eslint-disable-next-line camelcase + requested_predicates: {} + } + } + ] + }) + @IsObject({ each: true }) + @IsNotEmpty({ message: 'please provide valid proofFormat' }) + @IsOptional() + proofFormats?: IProofFormats; + + @ApiProperty({ + 'example': + { + id: '32f54163-7166-48f1-93d8-ff217bdb0653', + inputDescriptors: [ + { + 'id': 'banking_input_1', + 'name': 'Bank Account Information', + 'schema': [ + { + 'uri': 'https://bank-schemas.org/1.0.0/accounts.json' + } + + ], + 'constraints': { + 'fields': [ + { + 'path': ['$.issuer'] + } + ] + } + } + ] + }, + type: () => [ProofRequestPresentationDefinition] + }) + @IsOptional() + @ValidateNested() + @IsObject({ message: 'presentationDefinition must be an object' }) + @IsNotEmpty({ message: 'presentationDefinition must not be empty' }) + @Type(() => ProofRequestPresentationDefinition) + presentationDefinition?:ProofRequestPresentationDefinition; + + type:string; + + @ApiPropertyOptional() + @IsString({ message: 'auto accept proof must be in string' }) + @IsNotEmpty({ message: 'please provide from valid auto accept proof options' }) + @IsOptional() + @IsEnum(AutoAccept, { + message: `Invalid auto accept proof. It should be one of: ${Object.values(AutoAccept).join(', ')}` + }) + autoAcceptProof: AutoAccept; + + @ApiPropertyOptional() + @IsOptional() + @IsString({ message: 'label must be in string' }) + label: string; + + @ApiPropertyOptional() + @IsOptional() + @IsUUID() + @IsNotEmpty({ message: 'please provide valid parentThreadId' }) + parentThreadId: string; + + @ApiProperty({ example: true }) + @IsBoolean() + @IsOptional() + @IsNotEmpty({message:'Please provide the flag for shorten url.'}) + isShortenUrl?: boolean; + + @ApiPropertyOptional() + @IsEmail({}, { each: true, message: 'Please provide a valid email' }) + @ArrayNotEmpty({ message: 'Email array must not be empty' }) + @ArrayUnique({ message: 'Duplicate emails are not allowed' }) + @ArrayMaxSize(Number(process.env.OOB_BATCH_SIZE), { message: `Limit reached (${process.env.OOB_BATCH_SIZE} proof request max).` }) + @IsArray() + @IsString({ each: true, message: 'Each emailId in the array should be a string' }) + @IsOptional() + emailId: string[]; + + @ApiPropertyOptional({ default: true }) + @IsOptional() + @IsNotEmpty({ message: 'please provide valid value for reuseConnection' }) + @IsBoolean({ message: 'reuseConnection must be a boolean' }) + reuseConnection?: boolean; + +} diff --git a/apps/api-gateway/src/verification/dto/webhook-proof.dto.ts b/apps/api-gateway/src/verification/dto/webhook-proof.dto.ts index 45564ddfc..960fbbf13 100644 --- a/apps/api-gateway/src/verification/dto/webhook-proof.dto.ts +++ b/apps/api-gateway/src/verification/dto/webhook-proof.dto.ts @@ -64,4 +64,8 @@ export class WebhookPresentationProofDto { @ApiPropertyOptional() @IsOptional() type: string; + + @ApiPropertyOptional() + @IsOptional() + proofData: object; } \ No newline at end of file diff --git a/apps/api-gateway/src/verification/enum/verification.enum.ts b/apps/api-gateway/src/verification/enum/verification.enum.ts index bcb8ab6fc..87c8b88a2 100644 --- a/apps/api-gateway/src/verification/enum/verification.enum.ts +++ b/apps/api-gateway/src/verification/enum/verification.enum.ts @@ -3,4 +3,9 @@ export enum SortFields { STATUS = 'state', CONNECTION_ID = 'connectionId', PRESENTATION_ID = 'presentationId' +} + +export enum ProofRequestType { + INDY = 'indy', + PRESENTATIONEXCHANGE = 'presentationExchange' } \ No newline at end of file diff --git a/apps/api-gateway/src/verification/interfaces/verification.interface.ts b/apps/api-gateway/src/verification/interfaces/verification.interface.ts index 7937c6aee..a722da72a 100644 --- a/apps/api-gateway/src/verification/interfaces/verification.interface.ts +++ b/apps/api-gateway/src/verification/interfaces/verification.interface.ts @@ -51,3 +51,42 @@ interface ITags { state: string; threadId: string; } + +export interface IProofFormats { + indy: IndyProof +} + +interface IndyProof { + name: string; + version: string; + requested_attributes: IRequestedAttributes; + requested_predicates: IRequestedPredicates; +} + +interface IRequestedAttributes { + [key: string]: IRequestedAttributesName; +} + +interface IRequestedAttributesName { + name?: string; + names?: string; + restrictions: IRequestedRestriction[] +} + +interface IRequestedPredicates { + [key: string]: IRequestedPredicatesName; +} + +interface IRequestedPredicatesName { + name: string; + restrictions: IRequestedRestriction[] +} + +interface IRequestedRestriction { + cred_def_id?: string; + schema_id?: string; + schema_issuer_did?: string; + schema_name?: string; + issuer_did?: string; + schema_version?: string; +} \ No newline at end of file diff --git a/apps/api-gateway/src/verification/verification.controller.ts b/apps/api-gateway/src/verification/verification.controller.ts index 4f55727d4..bed9126f0 100644 --- a/apps/api-gateway/src/verification/verification.controller.ts +++ b/apps/api-gateway/src/verification/verification.controller.ts @@ -16,7 +16,7 @@ import { Controller, Logger, Post, Body, Get, Query, HttpStatus, Res, UseGuards, import { ApiResponseDto } from '../dtos/apiResponse.dto'; import { UnauthorizedErrorDto } from '../dtos/unauthorized-error.dto'; import { ForbiddenErrorDto } from '../dtos/forbidden-error.dto'; -import { OutOfBandRequestProof, RequestProofDto } from './dto/request-proof.dto'; +import { SendProofRequestPayload, RequestProofDto } from './dto/request-proof.dto'; import { VerificationService } from './verification.service'; import IResponseType, { IResponse } from '@credebl/common/interfaces/response.interface'; import { Response } from 'express'; @@ -32,7 +32,7 @@ import { ImageServiceService } from '@credebl/image-service'; import { User } from '../authz/decorators/user.decorator'; import { GetAllProofRequestsDto } from './dto/get-all-proof-requests.dto'; import { IProofRequestSearchCriteria } from './interfaces/verification.interface'; -import { SortFields } from './enum/verification.enum'; +import { ProofRequestType, SortFields } from './enum/verification.enum'; @UseFilters(CustomExceptionFilter) @Controller() @@ -188,8 +188,7 @@ export class VerificationController { } else { throw new BadRequestException('Please provide unique attribute names'); } - - await this.validateAttribute(attrData); + } requestProof.orgId = orgId; @@ -247,22 +246,23 @@ export class VerificationController { @ApiResponse({ status: HttpStatus.CREATED, description: 'Success', type: ApiResponseDto }) @ApiUnauthorizedResponse({ status: HttpStatus.UNAUTHORIZED, description: 'Unauthorized', type: UnauthorizedErrorDto }) @ApiForbiddenResponse({ status: HttpStatus.FORBIDDEN, description: 'Forbidden', type: ForbiddenErrorDto }) - @ApiBody({ type: OutOfBandRequestProof }) + @ApiBody({ type: SendProofRequestPayload }) + @ApiQuery({ + name: 'requestType', + enum: ProofRequestType + }) @Roles(OrgRoles.OWNER, OrgRoles.ADMIN, OrgRoles.VERIFIER) @ApiBearerAuth() @UseGuards(AuthGuard('jwt'), OrgRolesGuard) async sendOutOfBandPresentationRequest( @Res() res: Response, @User() user: IUserRequest, - @Body() outOfBandRequestProof: OutOfBandRequestProof, - @Param('orgId') orgId: string + @Body() outOfBandRequestProof: SendProofRequestPayload, + @Param('orgId') orgId: string, + @Query('requestType') requestType:ProofRequestType = ProofRequestType.INDY ): Promise { - - for (const attrData of outOfBandRequestProof.attributes) { - await this.validateAttribute(attrData); - } - - outOfBandRequestProof.orgId = orgId; + user.orgId = orgId; + outOfBandRequestProof.type = requestType; const result = await this.verificationService.sendOutOfBandPresentationRequest(outOfBandRequestProof, user); const finalResponse: IResponseType = { statusCode: HttpStatus.CREATED, @@ -292,7 +292,6 @@ export class VerificationController { @Res() res: Response ): Promise { proofPresentationPayload.type = 'Verification'; - this.logger.debug(`proofPresentationPayload ::: ${JSON.stringify(proofPresentationPayload)}`); const webhookProofPresentation = await this.verificationService.webhookProofPresentation(orgId, proofPresentationPayload).catch(error => { this.logger.debug(`error in saving verification webhook ::: ${JSON.stringify(error)}`); @@ -318,27 +317,4 @@ export class VerificationController { return res.status(HttpStatus.CREATED).json(finalResponse); } - - async validateAttribute( - attrData: object - ): Promise { - - if (!attrData['attributeName']) { - throw new BadRequestException('attributeName must be required'); - } - - if (undefined !== attrData['condition'] && '' === attrData['condition'].trim()) { - throw new BadRequestException('condition cannot be empty'); - } - - if (undefined !== attrData['value'] && '' === attrData['value'].trim()) { - throw new BadRequestException('value cannot be empty'); - } - - if (attrData['condition']) { - if (isNaN(attrData['value'])) { - throw new BadRequestException('value must be an integer'); - } - } - } } \ No newline at end of file diff --git a/apps/api-gateway/src/verification/verification.service.ts b/apps/api-gateway/src/verification/verification.service.ts index 9faea8c2b..8a51651b4 100644 --- a/apps/api-gateway/src/verification/verification.service.ts +++ b/apps/api-gateway/src/verification/verification.service.ts @@ -1,7 +1,7 @@ import { Injectable, Inject} from '@nestjs/common'; import { ClientProxy} from '@nestjs/microservices'; import { BaseService } from 'libs/service/base.service'; -import { OutOfBandRequestProof, RequestProofDto } from './dto/request-proof.dto'; +import { SendProofRequestPayload, RequestProofDto } from './dto/request-proof.dto'; import { IUserRequest } from '@credebl/user-request/user-request.interface'; import { WebhookPresentationProofDto } from './dto/webhook-proof.dto'; import { IProofPresentationDetails, IProofPresentationList } from '@credebl/common/interfaces/verification.interface'; @@ -70,7 +70,7 @@ export class VerificationService extends BaseService { * @param outOfBandRequestProof * @returns Get out-of-band requested proof presentation details */ - sendOutOfBandPresentationRequest(outOfBandRequestProof: OutOfBandRequestProof, user: IUserRequest): Promise { + sendOutOfBandPresentationRequest(outOfBandRequestProof: SendProofRequestPayload, user: IUserRequest): Promise { const payload = { outOfBandRequestProof, user }; return this.sendNatsMessage(this.verificationServiceProxy, 'send-out-of-band-proof-request', payload); } diff --git a/apps/connection/src/connection.controller.ts b/apps/connection/src/connection.controller.ts index 2e4f49b02..9b8c5e645 100644 --- a/apps/connection/src/connection.controller.ts +++ b/apps/connection/src/connection.controller.ts @@ -2,8 +2,10 @@ import { Controller } from '@nestjs/common'; // Import the common service in the import { ConnectionService } from './connection.service'; // Import the common service in connection module import { MessagePattern } from '@nestjs/microservices'; // Import the nestjs microservices package import { + GetAllConnections, IConnection, ICreateConnection, + ICreateOutOfbandConnectionInvitation, IFetchConnectionById, IFetchConnections, IReceiveInvitationByOrg, @@ -12,6 +14,7 @@ import { } from './interfaces/connection.interfaces'; import { IConnectionList, ICreateConnectionUrl } from '@credebl/common/interfaces/connection.interface'; import { IConnectionDetailsById } from 'apps/api-gateway/src/interfaces/IConnectionSearch.interface'; +import { IQuestionPayload } from './interfaces/question-answer.interfaces'; @Controller() export class ConnectionController { @@ -53,6 +56,12 @@ export class ConnectionController { return this.connectionService.getConnections(user, orgId, connectionSearchCriteria); } + @MessagePattern({ cmd: 'get-all-agent-connection-list' }) + async getConnectionListFromAgent(payload: GetAllConnections): Promise { + const {orgId, connectionSearchCriteria } = payload; + return this.connectionService.getAllConnectionListFromAgent(orgId, connectionSearchCriteria); + } + /** * * @param connectionId @@ -76,4 +85,19 @@ export class ConnectionController { const { user, receiveInvitation, orgId } = payload; return this.connectionService.receiveInvitation(user, receiveInvitation, orgId); } + + @MessagePattern({ cmd: 'send-question' }) + async sendQuestion(payload: IQuestionPayload): Promise { + return this.connectionService.sendQuestion(payload); + } + + @MessagePattern({ cmd: 'get-question-answer-record' }) + async getQuestionAnswersRecord(orgId: string): Promise { + return this.connectionService.getQuestionAnswersRecord(orgId); + } + + @MessagePattern({ cmd: 'create-connection-invitation' }) + async createConnectionInvitation(payload: ICreateOutOfbandConnectionInvitation): Promise { + return this.connectionService.createConnectionInvitation(payload); + } } diff --git a/apps/connection/src/connection.repository.ts b/apps/connection/src/connection.repository.ts index d28b6faf3..2b7034878 100644 --- a/apps/connection/src/connection.repository.ts +++ b/apps/connection/src/connection.repository.ts @@ -48,7 +48,8 @@ export class ConnectionRepository { async saveAgentConnectionInvitations( connectionInvitation: string, agentId: string, - orgId: string + orgId: string, + recipientKey: string // eslint-disable-next-line camelcase ): Promise { try { @@ -57,7 +58,8 @@ export class ConnectionRepository { orgId: String(orgId), agentId, connectionInvitation, - multiUse: true + multiUse: true, + recipientKey } }); return agentDetails; diff --git a/apps/connection/src/connection.service.ts b/apps/connection/src/connection.service.ts index 004a29527..1fbd983fe 100644 --- a/apps/connection/src/connection.service.ts +++ b/apps/connection/src/connection.service.ts @@ -5,13 +5,16 @@ import { HttpException, Inject, Injectable, Logger, NotFoundException } from '@n import { ClientProxy, RpcException } from '@nestjs/microservices'; import { map } from 'rxjs'; import { + ConnectionResponseDetail, + AgentConnectionSearchCriteria, IConnection, - IConnectionInvitation, IConnectionSearchCriteria, ICreateConnection, IReceiveInvitation, IReceiveInvitationResponse, - IReceiveInvitationUrl + IReceiveInvitationUrl, + ICreateOutOfbandConnectionInvitation, + ICreateConnectionInvitation } from './interfaces/connection.interfaces'; import { ConnectionRepository } from './connection.repository'; import { ResponseMessages } from '@credebl/common/response-messages'; @@ -21,6 +24,7 @@ import { Cache } from 'cache-manager'; import { CACHE_MANAGER } from '@nestjs/cache-manager'; import { IConnectionList, ICreateConnectionUrl } from '@credebl/common/interfaces/connection.interface'; import { IConnectionDetailsById } from 'apps/api-gateway/src/interfaces/IConnectionSearch.interface'; +import { IQuestionPayload } from './interfaces/question-answer.interfaces'; @Injectable() export class ConnectionService { @@ -30,7 +34,7 @@ export class ConnectionService { private readonly connectionRepository: ConnectionRepository, private readonly logger: Logger, @Inject(CACHE_MANAGER) private cacheService: Cache - ) { } + ) {} /** * Create connection legacy invitation URL @@ -39,14 +43,19 @@ export class ConnectionService { * @returns Connection legacy invitation URL */ async createLegacyConnectionInvitation(payload: IConnection): Promise { - - const { orgId, multiUseInvitation, autoAcceptConnection, alias, imageUrl, goal, goalCode, handshake, handshakeProtocols } = payload; + const { + orgId, + multiUseInvitation, + autoAcceptConnection, + alias, + imageUrl, + goal, + goalCode, + handshake, + handshakeProtocols, + recipientKey + } = payload; try { - const connectionInvitationExist = await this.connectionRepository.getConnectionInvitationByOrgId(orgId); - if (connectionInvitationExist) { - return connectionInvitationExist; - } - const agentDetails = await this.connectionRepository.getAgentEndPoint(orgId); const { agentEndPoint, id, organisation } = agentDetails; @@ -55,57 +64,52 @@ export class ConnectionService { throw new NotFoundException(ResponseMessages.connection.error.agentEndPointNotFound); } - let logoImageUrl; - if (organisation.logoUrl) { - logoImageUrl = organisation.logoUrl; - } - + this.logger.log(`logoUrl:::, ${organisation.logoUrl}`); const connectionPayload = { - multiUseInvitation: multiUseInvitation || true, - autoAcceptConnection: autoAcceptConnection || true, + multiUseInvitation: multiUseInvitation ?? true, + autoAcceptConnection: autoAcceptConnection ?? true, alias: alias || undefined, - imageUrl: logoImageUrl ? logoImageUrl : imageUrl ? imageUrl : undefined, + imageUrl: organisation.logoUrl || imageUrl || undefined, label: organisation.name, goal: goal || undefined, goalCode: goalCode || undefined, handshake: handshake || undefined, - handshakeProtocols: handshakeProtocols || undefined + handshakeProtocols: handshakeProtocols || undefined, + recipientKey: recipientKey || undefined }; const orgAgentType = await this.connectionRepository.getOrgAgentType(agentDetails?.orgAgentTypeId); const url = await this.getAgentUrl(orgAgentType, agentEndPoint, agentDetails?.tenantId); - - let apiKey: string = await this.cacheService.get(CommonConstants.CACHE_APIKEY_KEY); - if (!apiKey || null === apiKey || undefined === apiKey) { - apiKey = await this._getOrgAgentApiKey(orgId); - } - const createConnectionInvitation = await this._createConnectionInvitation(connectionPayload, url, apiKey); - const invitationObject = createConnectionInvitation?.message?.invitation['@id']; - let shortenedUrl; - if (agentDetails?.tenantId) { - shortenedUrl = `${agentEndPoint}/multi-tenancy/url/${agentDetails?.tenantId}/${invitationObject}`; - } else { - shortenedUrl = `${agentEndPoint}/url/${invitationObject}`; - } - + const createConnectionInvitation = await this._createConnectionInvitation(connectionPayload, url, orgId); + const connectionInvitationUrl = createConnectionInvitation?.response?.invitationUrl; + const shortenedUrl = await this.storeConnectionObjectAndReturnUrl( + connectionInvitationUrl, + connectionPayload.multiUseInvitation + ); + const recipientsKey = createConnectionInvitation?.response?.recipientKey || recipientKey; const saveConnectionDetails = await this.connectionRepository.saveAgentConnectionInvitations( shortenedUrl, agentId, - orgId + orgId, + recipientsKey ); - return saveConnectionDetails; + const connectionDetailRecords: ConnectionResponseDetail = { + id: saveConnectionDetails.id, + orgId: saveConnectionDetails.orgId, + agentId: saveConnectionDetails.agentId, + connectionInvitation: saveConnectionDetails.connectionInvitation, + multiUse: saveConnectionDetails.multiUse, + createDateTime: saveConnectionDetails.createDateTime, + createdBy: saveConnectionDetails.createdBy, + lastChangedDateTime: saveConnectionDetails.lastChangedDateTime, + lastChangedBy: saveConnectionDetails.lastChangedBy, + recordId: createConnectionInvitation.response.outOfBandRecord.id, + recipientKey: saveConnectionDetails.recipientKey + }; + return connectionDetailRecords; } catch (error) { this.logger.error(`[createLegacyConnectionInvitation] - error in connection invitation: ${error}`); - if (error && error?.status && error?.status?.message && error?.status?.message?.error) { - throw new RpcException({ - message: error?.status?.message?.error?.reason - ? error?.status?.message?.error?.reason - : error?.status?.message?.error, - statusCode: error?.status?.code - }); - } else { - throw new RpcException(error.response ? error.response : error); - } + this.handleError(error); } } @@ -132,17 +136,16 @@ export class ConnectionService { async _createConnectionInvitation( connectionPayload: object, url: string, - apiKey: string - ): Promise { - + orgId: string + ): Promise<{ + response; + }> { //nats call in agent-service to create an invitation url const pattern = { cmd: 'agent-create-connection-legacy-invitation' }; - const payload = { connectionPayload, url, apiKey }; + const payload = { connectionPayload, url, orgId }; try { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const message = await this.connectionServiceProxy.send(pattern, payload).toPromise(); - return { message }; + return await this.natsCall(pattern, payload); } catch (error) { this.logger.error(`catch: ${JSON.stringify(error)}`); throw new HttpException( @@ -229,9 +232,67 @@ export class ConnectionService { }; return connectionResponse; } catch (error) { + this.logger.error(`[getConnections] [NATS call]- error in fetch connections details : ${JSON.stringify(error)}`); + throw new RpcException(error.response ? error.response : error); + } + } + + async getAllConnectionListFromAgent( + orgId: string, + connectionSearchCriteria: AgentConnectionSearchCriteria + ): Promise { + try { + const { alias, myDid, outOfBandId, state, theirDid, theirLabel } = connectionSearchCriteria; + const agentDetails = await this.connectionRepository.getAgentEndPoint(orgId); + const orgAgentType = await this.connectionRepository.getOrgAgentType(agentDetails?.orgAgentTypeId); + const { agentEndPoint } = agentDetails; + if (!agentDetails) { + throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); + } + + let url: string; + if (orgAgentType === OrgAgentType.DEDICATED) { + url = `${agentEndPoint}${CommonConstants.URL_CONN_GET_CONNECTIONS}`; + } else if (orgAgentType === OrgAgentType.SHARED) { + url = `${agentEndPoint}${CommonConstants.URL_SHAGENT_GET_CREATEED_INVITATIONS}`.replace( + '#', + agentDetails.tenantId + ); + } else { + throw new NotFoundException(ResponseMessages.connection.error.agentUrlNotFound); + } + + //Create the dynamic URL for Search Criteria + const criteriaParams = []; + if (alias) { + criteriaParams.push(`alias=${alias}`); + } + if (myDid) { + criteriaParams.push(`myDid=${myDid}`); + } + if (outOfBandId) { + criteriaParams.push(`outOfBandId=${outOfBandId}`); + } + if (state) { + criteriaParams.push(`state=${state}`); + } + if (theirDid) { + criteriaParams.push(`theirDid=${theirDid}`); + } + if (theirLabel) { + criteriaParams.push(`theirLabel=${theirLabel}`); + } + + if (0 < criteriaParams.length) { + url += `?${criteriaParams.join('&')}`; + } + + const connectionResponse = await this._getAllConnections(url, orgId); + return connectionResponse.response; + } catch (error) { this.logger.error( - `[getConnections] [NATS call]- error in fetch connections details : ${JSON.stringify(error)}` + `[getConnectionsFromAgent] [NATS call]- error in fetch connections details : ${JSON.stringify(error)}` ); throw new RpcException(error.response ? error.response : error); @@ -240,31 +301,14 @@ export class ConnectionService { async _getAllConnections( url: string, - apiKey: string + orgId: string ): Promise<{ response: string; }> { try { const pattern = { cmd: 'agent-get-all-connections' }; - const payload = { url, apiKey }; - return this.connectionServiceProxy - .send(pattern, payload) - .pipe( - map((response) => ({ - response - })) - ) - .toPromise() - .catch((error) => { - this.logger.error(`catch: ${JSON.stringify(error)}`); - throw new HttpException( - { - status: error.statusCode, - error: error.message - }, - error.error - ); - }); + const payload = { url, orgId }; + return await this.natsCall(pattern, payload); } catch (error) { this.logger.error( `[_getAllConnections] [NATS call]- error in fetch connections details : ${JSON.stringify(error)}` @@ -277,7 +321,6 @@ export class ConnectionService { try { const agentDetails = await this.connectionRepository.getAgentEndPoint(orgId); const orgAgentType = await this.connectionRepository.getOrgAgentType(agentDetails?.orgAgentTypeId); - // const platformConfig: platform_config = await this.connectionRepository.getPlatformConfigDetails(); const { agentEndPoint } = agentDetails; if (!agentDetails) { @@ -295,16 +338,8 @@ export class ConnectionService { throw new NotFoundException(ResponseMessages.connection.error.agentUrlNotFound); } - - // const apiKey = await this._getOrgAgentApiKey(orgId); - let apiKey: string = await this.cacheService.get(CommonConstants.CACHE_APIKEY_KEY); - if (!apiKey || null === apiKey || undefined === apiKey) { - apiKey = await this._getOrgAgentApiKey(orgId); - } - const createConnectionInvitation = await this._getConnectionsByConnectionId(url, apiKey); - return createConnectionInvitation; - - + const createConnectionInvitation = await this._getConnectionsByConnectionId(url, orgId); + return createConnectionInvitation?.response; } catch (error) { this.logger.error(`[getConnectionsById] - error in get connections : ${JSON.stringify(error)}`); @@ -320,28 +355,42 @@ export class ConnectionService { } } + async getQuestionAnswersRecord(orgId: string): Promise { + try { + const agentDetails = await this.connectionRepository.getAgentEndPoint(orgId); + const orgAgentType = await this.connectionRepository.getOrgAgentType(agentDetails?.orgAgentTypeId); + const { agentEndPoint } = agentDetails; + if (!agentDetails) { + throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); + } + + const label = 'get-question-answer-record'; + const url = await this.getQuestionAnswerAgentUrl(label, orgAgentType, agentEndPoint, agentDetails?.tenantId); + + const record = await this._getQuestionAnswersRecord(url, orgId); + return record; + } catch (error) { + this.logger.error(`[sendQuestion] - error in get question answer record: ${error}`); + this.handleError(error); + } + } + async _getConnectionsByConnectionId( url: string, - apiKey: string - ): Promise { - + orgId: string + ): Promise<{ + response; + }> { //nats call in agent service for fetch connection details const pattern = { cmd: 'agent-get-connection-details-by-connectionId' }; - const payload = { url, apiKey }; - return this.connectionServiceProxy - .send(pattern, payload) - .toPromise() - .catch(error => { - this.logger.error( - `[_getConnectionsByConnectionId] [NATS call]- error in fetch connections : ${JSON.stringify(error)}` - ); - throw new HttpException( - { - status: error.statusCode, - error: error.error?.message?.error ? error.error?.message?.error : error.error, - message: error.message - }, error.error); - }); + const payload = { url, orgId }; + return this.natsCall(pattern, payload); + } + + async _getQuestionAnswersRecord(url: string, orgId: string): Promise { + const pattern = { cmd: 'agent-get-question-answer-record' }; + const payload = { url, orgId }; + return this.natsCall(pattern, payload); } /** @@ -349,15 +398,30 @@ export class ConnectionService { * @param referenceId * @returns agent URL */ - async getAgentUrl(orgAgentType: string, agentEndPoint: string, tenantId?: string): Promise { + async getAgentUrl( + orgAgentType: string, + agentEndPoint: string, + tenantId?: string, + connectionInvitationFlag?: string + ): Promise { try { let url; - if (orgAgentType === OrgAgentType.DEDICATED) { - url = `${agentEndPoint}${CommonConstants.URL_CONN_LEGACY_INVITE}`; - } else if (orgAgentType === OrgAgentType.SHARED) { - url = `${agentEndPoint}${CommonConstants.URL_SHAGENT_CREATE_INVITATION}`.replace('#', tenantId); + if ('connection-invitation' === connectionInvitationFlag) { + if (orgAgentType === OrgAgentType.DEDICATED) { + url = `${agentEndPoint}${CommonConstants.URL_CONN_INVITE}`; + } else if (orgAgentType === OrgAgentType.SHARED) { + url = `${agentEndPoint}${CommonConstants.URL_SHAGENT_CREATE_CONNECTION_INVITATION}`.replace('#', tenantId); + } else { + throw new NotFoundException(ResponseMessages.connection.error.agentUrlNotFound); + } } else { - throw new NotFoundException(ResponseMessages.connection.error.agentUrlNotFound); + if (orgAgentType === OrgAgentType.DEDICATED) { + url = `${agentEndPoint}${CommonConstants.URL_CONN_LEGACY_INVITE}`; + } else if (orgAgentType === OrgAgentType.SHARED) { + url = `${agentEndPoint}${CommonConstants.URL_SHAGENT_CREATE_INVITATION}`.replace('#', tenantId); + } else { + throw new NotFoundException(ResponseMessages.connection.error.agentUrlNotFound); + } } return url; } catch (error) { @@ -366,24 +430,79 @@ export class ConnectionService { } } - async _getOrgAgentApiKey(orgId: string): Promise { + async getQuestionAnswerAgentUrl( + label: string, + orgAgentType: string, + agentEndPoint: string, + tenantId?: string, + connectionId?: string + ): Promise { + try { + let url; + switch (label) { + case 'send-question': { + url = + orgAgentType === OrgAgentType.DEDICATED + ? `${agentEndPoint}${CommonConstants.URL_SEND_QUESTION}`.replace('#', connectionId) + : orgAgentType === OrgAgentType.SHARED + ? `${agentEndPoint}${CommonConstants.URL_SHAGENT_SEND_QUESTION}` + .replace('#', connectionId) + .replace('@', tenantId) + : null; + break; + } + + case 'get-question-answer-record': { + url = + orgAgentType === OrgAgentType.DEDICATED + ? `${agentEndPoint}${CommonConstants.URL_QUESTION_ANSWER_RECORD}` + : orgAgentType === OrgAgentType.SHARED + ? `${agentEndPoint}${CommonConstants.URL_SHAGENT_QUESTION_ANSWER_RECORD}`.replace('#', tenantId) + : null; + break; + } + + default: { + break; + } + } + + if (!url) { + throw new NotFoundException(ResponseMessages.issuance.error.agentUrlNotFound); + } + + return url; + } catch (error) { + this.logger.error(`Error get question answer agent Url: ${JSON.stringify(error)}`); + throw error; + } + } + + async _getOrgAgentApiKey(orgId: string): Promise<{ + response: string; + }> { const pattern = { cmd: 'get-org-agent-api-key' }; const payload = { orgId }; try { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const message = await this.connectionServiceProxy.send(pattern, payload).toPromise(); - return message; + return await this.natsCall(pattern, payload); } catch (error) { this.logger.error(`catch: ${JSON.stringify(error)}`); - throw new HttpException({ - status: error.status, - error: error.message - }, error.status); + throw new HttpException( + { + status: error.status, + error: error.message + }, + error.status + ); } } - async receiveInvitationUrl(user: IUserRequest, receiveInvitationUrl: IReceiveInvitationUrl, orgId: string): Promise { + async receiveInvitationUrl( + user: IUserRequest, + receiveInvitationUrl: IReceiveInvitationUrl, + orgId: string + ): Promise { try { const agentDetails = await this.connectionRepository.getAgentEndPoint(orgId); const orgAgentType = await this.connectionRepository.getOrgAgentType(agentDetails?.orgAgentTypeId); @@ -393,25 +512,20 @@ export class ConnectionService { throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); } - let url; if (orgAgentType === OrgAgentType.DEDICATED) { url = `${agentEndPoint}${CommonConstants.URL_RECEIVE_INVITATION_URL}`; } else if (orgAgentType === OrgAgentType.SHARED) { - url = `${agentEndPoint}${CommonConstants.URL_SHAGENT_RECEIVE_INVITATION_URL}` - .replace('#', agentDetails.tenantId); + url = `${agentEndPoint}${CommonConstants.URL_SHAGENT_RECEIVE_INVITATION_URL}`.replace( + '#', + agentDetails.tenantId + ); } else { throw new NotFoundException(ResponseMessages.connection.error.agentUrlNotFound); } - let apiKey: string = await this.cacheService.get(CommonConstants.CACHE_APIKEY_KEY); - if (!apiKey || null === apiKey || undefined === apiKey) { - apiKey = await this._getOrgAgentApiKey(orgId); - } - const createConnectionInvitation = await this._receiveInvitationUrl(url, apiKey, receiveInvitationUrl); - return createConnectionInvitation; - - + const createConnectionInvitation = await this._receiveInvitationUrl(url, orgId, receiveInvitationUrl); + return createConnectionInvitation.response; } catch (error) { this.logger.error(`[receiveInvitationUrl] - error in receive invitation url : ${JSON.stringify(error)}`); @@ -429,29 +543,21 @@ export class ConnectionService { async _receiveInvitationUrl( url: string, - apiKey: string, + orgId: string, receiveInvitationUrl: IReceiveInvitationUrl - ): Promise { - + ): Promise<{ + response; + }> { const pattern = { cmd: 'agent-receive-invitation-url' }; - const payload = { url, apiKey, receiveInvitationUrl }; - return this.connectionServiceProxy - .send(pattern, payload) - .toPromise() - .catch(error => { - this.logger.error( - `[_receiveInvitationUrl] [NATS call]- error in receive invitation url : ${JSON.stringify(error)}` - ); - throw new HttpException( - { - status: error.statusCode, - error: error.error?.message?.error ? error.error?.message?.error : error.error, - message: error.message - }, error.error); - }); + const payload = { url, orgId, receiveInvitationUrl }; + return this.natsCall(pattern, payload); } - async receiveInvitation(user: IUserRequest, receiveInvitation: IReceiveInvitation, orgId: string): Promise { + async receiveInvitation( + user: IUserRequest, + receiveInvitation: IReceiveInvitation, + orgId: string + ): Promise { try { const agentDetails = await this.connectionRepository.getAgentEndPoint(orgId); const orgAgentType = await this.connectionRepository.getOrgAgentType(agentDetails?.orgAgentTypeId); @@ -465,20 +571,13 @@ export class ConnectionService { if (orgAgentType === OrgAgentType.DEDICATED) { url = `${agentEndPoint}${CommonConstants.URL_RECEIVE_INVITATION}`; } else if (orgAgentType === OrgAgentType.SHARED) { - url = `${agentEndPoint}${CommonConstants.URL_SHAGENT_RECEIVE_INVITATION}` - .replace('#', agentDetails.tenantId); + url = `${agentEndPoint}${CommonConstants.URL_SHAGENT_RECEIVE_INVITATION}`.replace('#', agentDetails.tenantId); } else { throw new NotFoundException(ResponseMessages.connection.error.agentUrlNotFound); } - let apiKey: string = await this.cacheService.get(CommonConstants.CACHE_APIKEY_KEY); - if (!apiKey || null === apiKey || undefined === apiKey) { - apiKey = await this._getOrgAgentApiKey(orgId); - } - const createConnectionInvitation = await this._receiveInvitation(url, apiKey, receiveInvitation); - return createConnectionInvitation; - - + const createConnectionInvitation = await this._receiveInvitation(url, orgId, receiveInvitation); + return createConnectionInvitation?.response; } catch (error) { this.logger.error(`[receiveInvitation] - error in receive invitation : ${JSON.stringify(error)}`); @@ -496,26 +595,247 @@ export class ConnectionService { async _receiveInvitation( url: string, - apiKey: string, + orgId: string, receiveInvitation: IReceiveInvitation - ): Promise { - + ): Promise<{ + response; + }> { const pattern = { cmd: 'agent-receive-invitation' }; - const payload = { url, apiKey, receiveInvitation }; - return this.connectionServiceProxy - .send(pattern, payload) - .toPromise() - .catch(error => { - this.logger.error( - `[_receiveInvitation] [NATS call]- error in receive invitation : ${JSON.stringify(error)}` - ); - throw new HttpException( - { - status: error.statusCode, - error: error.error?.message?.error ? error.error?.message?.error : error.error, - message: error.message - }, error.error); + const payload = { url, orgId, receiveInvitation }; + return this.natsCall(pattern, payload); + } + + async _sendQuestion(questionPayload: IQuestionPayload, url: string, orgId: string): Promise { + const pattern = { cmd: 'agent-send-question' }; + const payload = { questionPayload, url, orgId }; + return this.natsCall(pattern, payload); + } + + async sendQuestion(payload: IQuestionPayload): Promise { + const { detail, validResponses, question, orgId, connectionId } = payload; + try { + const agentDetails = await this.connectionRepository.getAgentEndPoint(orgId); + + const { agentEndPoint } = agentDetails; + + if (!agentDetails) { + throw new NotFoundException(ResponseMessages.connection.error.agentEndPointNotFound); + } + + const questionPayload = { + detail, + validResponses, + question + }; + + const orgAgentType = await this.connectionRepository.getOrgAgentType(agentDetails?.orgAgentTypeId); + const label = 'send-question'; + const url = await this.getQuestionAnswerAgentUrl( + label, + orgAgentType, + agentEndPoint, + agentDetails?.tenantId, + connectionId + ); + + const createQuestion = await this._sendQuestion(questionPayload, url, orgId); + return createQuestion; + } catch (error) { + this.logger.error(`[sendQuestion] - error in sending question: ${error}`); + if (error && error?.status && error?.status?.message && error?.status?.message?.error) { + throw new RpcException({ + message: error?.status?.message?.error?.reason + ? error?.status?.message?.error?.reason + : error?.status?.message?.error, + statusCode: error?.status?.code + }); + } else { + throw new RpcException(error.response ? error.response : error); + } + } + } + + async storeConnectionObjectAndReturnUrl(connectionInvitationUrl: string, persistent: boolean): Promise { + const storeObj = connectionInvitationUrl; + //nats call in agent-service to create an invitation url + const pattern = { cmd: 'store-object-return-url' }; + const payload = { persistent, storeObj }; + + try { + const message = await this.natsCall(pattern, payload); + return message.response; + } catch (error) { + this.logger.error(`catch: ${JSON.stringify(error)}`); + throw new HttpException( + { + status: error.status, + error: error.message + }, + error.status + ); + } + } + + /** + * Create connection invitation URL + * @param orgId + * @param user + * @returns Connection invitation URL + */ + async createConnectionInvitation(payload: ICreateOutOfbandConnectionInvitation): Promise { + try { + const { createOutOfBandConnectionInvitation } = payload; + const { + alias, + appendedAttachments, + autoAcceptConnection, + goal, + goalCode, + handshake, + handshakeProtocols, + imageUrl, + messages, + multiUseInvitation, + orgId, + routing + } = createOutOfBandConnectionInvitation; + + const agentDetails = await this.connectionRepository.getAgentEndPoint( + payload?.createOutOfBandConnectionInvitation?.orgId + ); + + const { agentEndPoint, id, organisation } = agentDetails; + const agentId = id; + if (!agentDetails) { + throw new NotFoundException(ResponseMessages.connection.error.agentEndPointNotFound); + } + + const connectionPayload = { + multiUseInvitation: multiUseInvitation ?? true, + autoAcceptConnection: autoAcceptConnection ?? true, + alias: alias || undefined, + imageUrl: organisation.logoUrl || imageUrl || undefined, + label: organisation.name, + goal: goal || undefined, + goalCode: goalCode || undefined, + handshake: handshake || undefined, + handshakeProtocols: handshakeProtocols || undefined, + appendedAttachments: appendedAttachments || undefined, + routing: routing || undefined, + messages: messages || undefined + }; + + const createConnectionInvitationFlag = 'connection-invitation'; + const orgAgentType = await this.connectionRepository.getOrgAgentType(agentDetails?.orgAgentTypeId); + const url = await this.getAgentUrl( + orgAgentType, + agentEndPoint, + agentDetails?.tenantId, + createConnectionInvitationFlag + ); + const createConnectionInvitation = await this._createOutOfBandConnectionInvitation(connectionPayload, url, orgId); + const connectionInvitationUrl = createConnectionInvitation?.response?.invitationUrl; + const shortenedUrl = await this.storeConnectionObjectAndReturnUrl( + connectionInvitationUrl, + connectionPayload.multiUseInvitation + ); + const saveConnectionDetails = await this.connectionRepository.saveAgentConnectionInvitations( + shortenedUrl, + agentId, + orgId, + null + ); + const connectionStorePayload: ConnectionResponseDetail = { + id: saveConnectionDetails.id, + orgId: saveConnectionDetails.orgId, + agentId: saveConnectionDetails.agentId, + connectionInvitation: saveConnectionDetails.connectionInvitation, + multiUse: saveConnectionDetails.multiUse, + createDateTime: saveConnectionDetails.createDateTime, + createdBy: saveConnectionDetails.createdBy, + lastChangedDateTime: saveConnectionDetails.lastChangedDateTime, + lastChangedBy: saveConnectionDetails.lastChangedBy, + recordId: createConnectionInvitation.response.outOfBandRecord.id, + recipientKey: saveConnectionDetails.recipientKey + }; + return connectionStorePayload; + } catch (error) { + this.logger.error(`[createConnectionInvitation] - error in connection oob invitation: ${error}`); + this.handleError(error); + } + } + + /** + * Store shortening URL + * @param orgId + * @returns connection invitation URL + */ + async _createOutOfBandConnectionInvitation( + connectionPayload: ICreateConnectionInvitation, + url: string, + orgId: string + ): Promise<{ + response; + }> { + //nats call in agent-service to create an invitation url + const pattern = { cmd: 'agent-create-connection-invitation' }; + const payload = { connectionPayload, url, orgId }; + + try { + return await this.natsCall(pattern, payload); + } catch (error) { + this.logger.error(`catch: ${JSON.stringify(error)}`); + throw new HttpException( + { + status: error.status, + error: error.message + }, + error.status + ); + } + } + + async natsCall( + pattern: object, + payload: object + ): Promise<{ + response: string; + }> { + try { + return this.connectionServiceProxy + .send(pattern, payload) + .pipe( + map((response) => ({ + response + })) + ) + .toPromise() + .catch((error) => { + this.logger.error(`catch: ${JSON.stringify(error)}`); + throw new HttpException( + { + status: error.statusCode, + error: error.message + }, + error.error + ); + }); + } catch (error) { + this.logger.error(`[ConnectionService natsCall] - error in nats call : ${JSON.stringify(error)}`); + throw error; + } + } + + handleError(error): Promise { + if (error && error?.status && error?.status?.message && error?.status?.message?.error) { + throw new RpcException({ + message: error?.status?.message?.error?.reason + ? error?.status?.message?.error?.reason + : error?.status?.message?.error, + statusCode: error?.status?.code }); + } else { + throw new RpcException(error.response ? error.response : error); + } } } - diff --git a/apps/connection/src/interfaces/connection.interfaces.ts b/apps/connection/src/interfaces/connection.interfaces.ts index f26a6fd2f..af3f24a76 100644 --- a/apps/connection/src/interfaces/connection.interfaces.ts +++ b/apps/connection/src/interfaces/connection.interfaces.ts @@ -15,6 +15,7 @@ export interface IConnection { handshake: string; handshakeProtocols: string[]; orgId: string; + recipientKey?: string; } export interface IUserRequestInterface { userId: string; @@ -80,6 +81,12 @@ export interface IFetchConnections { orgId: string; } +export interface GetAllConnections { + connectionSearchCriteria: AgentConnectionSearchCriteria; + user: IUserRequest; + orgId: string; +} + export interface IFetchConnectionById { user: IUserRequest; connectionId: string; @@ -97,8 +104,9 @@ export interface IConnectionInvitation { } interface IInvitation { invitation: string; - + invitationUrl: string; } + export interface OrgAgent { organisation: organisation; id: string; @@ -124,6 +132,15 @@ export interface IConnectionSearchCriteria { user: IUserRequestInterface } +export interface AgentConnectionSearchCriteria { + outOfBandId: string; + alias: string; + state: string; + myDid: string; + theirDid: string; + theirLabel: string; +} + export interface IReceiveInvitationByUrlOrg { user: IUserRequestInterface, receiveInvitationUrl: IReceiveInvitationUrl, @@ -236,3 +253,38 @@ export interface IReceiveInvitationResponse { outOfBandRecord: OutOfBandRecord; connectionRecord: ConnectionRecord; } + +export interface ConnectionResponseDetail { + id: string; + orgId: string; + agentId: string; + connectionInvitation: string; + multiUse: boolean; + createDateTime: Date; + createdBy: number; + lastChangedDateTime: Date; + lastChangedBy: number; + recordId: string; + recipientKey:string; +} + +export interface ICreateConnectionInvitation { + label?: string; + alias?: string; + imageUrl?: string; + goalCode?: string; + goal?: string; + handshake?: boolean; + handshakeProtocols?: object[]; + messages?: object[]; + multiUseInvitation?: boolean; + autoAcceptConnection?: boolean; + routing?: object; + appendedAttachments?: object[]; + orgId?: string; +} + +export interface ICreateOutOfbandConnectionInvitation { + user: IUserRequestInterface, + createOutOfBandConnectionInvitation: ICreateConnectionInvitation, +} \ No newline at end of file diff --git a/apps/connection/src/interfaces/question-answer.interfaces.ts b/apps/connection/src/interfaces/question-answer.interfaces.ts new file mode 100644 index 000000000..6fcabd9ef --- /dev/null +++ b/apps/connection/src/interfaces/question-answer.interfaces.ts @@ -0,0 +1,11 @@ +export interface IValidResponses { + text: string; + } + export interface IQuestionPayload { + detail: string; + validResponses: IValidResponses[]; + question: string; + orgId?: string; + connectionId?: string; + tenantId?: string; + } diff --git a/apps/ecosystem/dtos/accept-reject-ecosysteminvitation.dto.ts b/apps/ecosystem/dtos/accept-reject-ecosysteminvitation.dto.ts index 1bc76de4f..99bcc61c5 100644 --- a/apps/ecosystem/dtos/accept-reject-ecosysteminvitation.dto.ts +++ b/apps/ecosystem/dtos/accept-reject-ecosysteminvitation.dto.ts @@ -7,4 +7,5 @@ export class AcceptRejectEcosystemInvitationDto { orgName?: string; orgDid?: string; userId?: string; + userEmail?: string; } diff --git a/apps/ecosystem/interfaces/ecosystem.interfaces.ts b/apps/ecosystem/interfaces/ecosystem.interfaces.ts index 11ad4b73a..4fa335baf 100644 --- a/apps/ecosystem/interfaces/ecosystem.interfaces.ts +++ b/apps/ecosystem/interfaces/ecosystem.interfaces.ts @@ -220,41 +220,6 @@ export interface LedgerDetails { networkUrl: string; } -interface EcosystemRole { - id: string; - name: string; - description: string; - createDateTime: Date; - lastChangedDateTime: Date; - deletedAt: Date; -} - -interface EcosystemDetail { - id: string; - name: string; - description: string; - logoUrl: string; - createDateTime: Date; - lastChangedDateTime: Date; - createdBy: string; - autoEndorsement: boolean; - ecosystemOrgs: { - id: string; - orgId: string; - status: string; - createDateTime: Date; - lastChangedDateTime: Date; - ecosystemId: string; - ecosystemRoleId: string; - ecosystemRole: EcosystemRole; - }[]; -} - -export interface EcosystemDetailsResult { - totalCount: number; - ecosystemDetails: EcosystemDetail[]; -} - export interface EcosystemInvitationDetails { name: string; id: string; @@ -363,4 +328,44 @@ export interface IEcosystemInvitations { ecosystem: EcosystemInvitationDetails; createDateTime: Date; createdBy: string; -} \ No newline at end of file +} + +interface IAttributes { + isRequired: boolean; + displayName: string; + attributeName: string; + schemaDataType: string; +} + +interface ISChemaItems { + id: string; + createDateTime: string; + createdBy: string; + lastChangedDateTime: string; + lastChangedBy: string; + name: string; + version: string; + attributes: IAttributes[]; + schemaLedgerId: string; + publisherDid: string; + issuerId: string; + orgId: string; + ledgerId: string; +} + +export interface ISchemaResponse { + totalItems: number; + hasNextPage: boolean; + hasPreviousPage: boolean; + nextPage: number; + previousPage: number; + lastPage: number; + data: ISChemaItems[]; +} + +export interface IEcosystemList { + orgId: string, + pageNumber: number; + pageSize: number; + search: string; +} diff --git a/apps/ecosystem/interfaces/ecosystemMembers.interface.ts b/apps/ecosystem/interfaces/ecosystemMembers.interface.ts index 8ea39f7cf..58b12fd4b 100644 --- a/apps/ecosystem/interfaces/ecosystemMembers.interface.ts +++ b/apps/ecosystem/interfaces/ecosystemMembers.interface.ts @@ -5,4 +5,5 @@ export interface EcosystemMembersPayload { pageSize: number; search: string; sortBy: string; + sortField: string; } \ No newline at end of file diff --git a/apps/ecosystem/interfaces/endorsements.interface.ts b/apps/ecosystem/interfaces/endorsements.interface.ts index a8024a05e..fe3ba0356 100644 --- a/apps/ecosystem/interfaces/endorsements.interface.ts +++ b/apps/ecosystem/interfaces/endorsements.interface.ts @@ -15,4 +15,22 @@ export interface GetEndorsementsPayload { pageNumber: number; pageSize: number; search: string; - } \ No newline at end of file + } + + interface ISchemaResult { + createDateTime: Date; + createdBy: string; + name: string; + version: string; + attributes: string; + schemaLedgerId: string; + publisherDid: string; + issuerId: string; + orgId: string; + } + + export interface ISchemasResponse { + schemasCount: number; + schemasResult: ISchemaResult[]; + } + \ No newline at end of file diff --git a/apps/ecosystem/src/ecosystem.controller.ts b/apps/ecosystem/src/ecosystem.controller.ts index db3ab3281..95513b5d7 100644 --- a/apps/ecosystem/src/ecosystem.controller.ts +++ b/apps/ecosystem/src/ecosystem.controller.ts @@ -7,8 +7,9 @@ import { BulkSendInvitationDto } from '../dtos/send-invitation.dto'; import { AcceptRejectEcosystemInvitationDto } from '../dtos/accept-reject-ecosysteminvitation.dto'; import { FetchInvitationsPayload } from '../interfaces/invitations.interface'; import { EcosystemMembersPayload } from '../interfaces/ecosystemMembers.interface'; -import { GetEndorsementsPayload } from '../interfaces/endorsements.interface'; -import { IEcosystemDashboard, RequestCredDeffEndorsement, RequestSchemaEndorsement, IEcosystem, EcosystemDetailsResult, IEcosystemInvitation, IEcosystemInvitations, IEditEcosystem, IEndorsementTransaction } from '../interfaces/ecosystem.interfaces'; +import { GetEndorsementsPayload, ISchemasResponse } from '../interfaces/endorsements.interface'; +import { IEcosystemDashboard, RequestCredDeffEndorsement, RequestSchemaEndorsement, IEcosystem, IEcosystemInvitation, IEcosystemInvitations, IEditEcosystem, IEndorsementTransaction, IEcosystemList } from '../interfaces/ecosystem.interfaces'; +import { IEcosystemDetails } from '@credebl/common/interfaces/ecosystem.interface'; @Controller() export class EcosystemController { @@ -42,7 +43,7 @@ export class EcosystemController { * @returns Get all ecosystem details */ @MessagePattern({ cmd: 'get-all-ecosystem' }) - async getAllEcosystems(@Body() payload: { orgId: string }): Promise { + async getAllEcosystems(@Body() payload: IEcosystemList): Promise { return this.ecosystemService.getAllEcosystem(payload); } @@ -79,7 +80,7 @@ export class EcosystemController { */ @MessagePattern({ cmd: 'fetch-ecosystem-members' }) async getEcosystemMembers(@Body() payload: EcosystemMembersPayload): Promise { - return this.ecosystemService.getEcoystemMembers(payload); + return this.ecosystemService.getEcosystemMembers(payload); } /** @@ -104,9 +105,10 @@ export class EcosystemController { */ @MessagePattern({ cmd: 'accept-reject-ecosystem-invitations' }) async acceptRejectEcosystemInvitations(payload: { - acceptRejectInvitation: AcceptRejectEcosystemInvitationDto; + acceptRejectInvitation: AcceptRejectEcosystemInvitationDto, + userEmail: string }): Promise { - return this.ecosystemService.acceptRejectEcosystemInvitations(payload.acceptRejectInvitation); + return this.ecosystemService.acceptRejectEcosystemInvitations(payload.acceptRejectInvitation, payload.userEmail); } @MessagePattern({ cmd: 'get-sent-invitations-ecosystemId' }) @@ -120,7 +122,7 @@ export class EcosystemController { } @MessagePattern({ cmd: 'get-all-ecosystem-schemas' }) - async getAllEcosystemSchemas(@Body() payload: GetEndorsementsPayload): Promise { + async getAllEcosystemSchemas(@Body() payload: GetEndorsementsPayload): Promise { return this.ecosystemService.getAllEcosystemSchemas(payload); } diff --git a/apps/ecosystem/src/ecosystem.repository.ts b/apps/ecosystem/src/ecosystem.repository.ts index da17cd2f0..45085caa4 100644 --- a/apps/ecosystem/src/ecosystem.repository.ts +++ b/apps/ecosystem/src/ecosystem.repository.ts @@ -4,11 +4,12 @@ import { PrismaService } from '@credebl/prisma-service'; import { credential_definition, ecosystem, ecosystem_config, ecosystem_invitations, ecosystem_orgs, ecosystem_roles, endorsement_transaction, org_agents, platform_config, schema } from '@prisma/client'; import { DeploymentModeType, EcosystemInvitationStatus, EcosystemOrgStatus, EcosystemRoles, endorsementTransactionStatus, endorsementTransactionType } from '../enums/ecosystem.enum'; import { updateEcosystemOrgsDto } from '../dtos/update-ecosystemOrgs.dto'; -import { CreateEcosystem, IEcosystemInvitation, EcosystemDetailsResult, SaveSchema, SchemaTransactionResponse, saveCredDef } from '../interfaces/ecosystem.interfaces'; +import { CreateEcosystem, IEcosystemInvitation, SaveSchema, SchemaTransactionResponse, saveCredDef } from '../interfaces/ecosystem.interfaces'; import { ResponseMessages } from '@credebl/common/response-messages'; import { NotFoundException } from '@nestjs/common'; import { CommonConstants } from '@credebl/common/common.constant'; -import { GetAllSchemaList } from '../interfaces/endorsements.interface'; +import { GetAllSchemaList, ISchemasResponse } from '../interfaces/endorsements.interface'; +import { SortValue } from '@credebl/enum/enum'; // eslint-disable-next-line camelcase @Injectable() @@ -112,65 +113,77 @@ export class EcosystemRepository { * * @returns Get all ecosystem details */ - // eslint-disable-next-line camelcase - async getAllEcosystemDetails(orgId: string): Promise { - try { - const [ecosystemDetails, ecosystemCount] = await Promise.all([ - this.prisma.ecosystem.findMany({ - where: { - ecosystemOrgs: { - some: { - orgId - } - } - }, - select: { - id: true, - name: true, - description: true, - logoUrl: true, - createDateTime: true, - lastChangedDateTime: true, - createdBy: true, - autoEndorsement: true, - ecosystemOrgs: { - where: { - orgId + async getAllEcosystemDetails( + orgId: string, + pageNumber: number, + pageSize: number, + search: string + ): Promise { + try { + const sortByName = SortValue.DESC; + + const result = await Promise.all([ + this.prisma.ecosystem.findMany({ + where: { + ecosystemOrgs: { + some: { + orgId + } }, - select: { - id: true, - orgId: true, - status: true, - createDateTime: true, - lastChangedDateTime: true, - ecosystemId: true, - ecosystemRoleId: true, - ecosystemRole: true + OR: [ + { name: { contains: search, mode: 'insensitive' } }, + { description: { contains: search, mode: 'insensitive' } } + ] + }, + select: { + id: true, + name: true, + description: true, + logoUrl: true, + createDateTime: true, + lastChangedDateTime: true, + createdBy: true, + autoEndorsement: true, + ecosystemOrgs: { + where: { + orgId + }, + select: { + id: true, + orgId: true, + status: true, + createDateTime: true, + lastChangedDateTime: true, + ecosystemId: true, + ecosystemRoleId: true, + ecosystemRole: true + } } + }, + take: Number(pageSize), + skip: (pageNumber - 1) * pageSize, + orderBy: { + name: sortByName } - } - }), - this.prisma.ecosystem.count({ - where: { - ecosystemOrgs: { - some: { - orgId + }), + this.prisma.ecosystem.count({ + where: { + ecosystemOrgs: { + some: { + orgId + } } } - } - }) - ]); - - return { - ecosystemDetails, - totalCount: ecosystemCount - }; - } catch (error) { - this.logger.error(`Error in get all ecosystem transaction: ${error.message}`); - throw error; - } + }) + ]); + + return result; + } catch (error) { + this.logger.error(`Error in get all ecosystem transaction: ${error.message}`); + throw error; + } } - + /** * @@ -355,11 +368,12 @@ export class EcosystemRepository { * @returns Invitation details */ // eslint-disable-next-line camelcase - async getEcosystemInvitationById(id: string): Promise { + async getEcosystemInvitationById(id: string, email: string): Promise { try { return this.prisma.ecosystem_invitations.findUnique({ where: { - id + id, + email }, include: { ecosystem: true @@ -486,62 +500,67 @@ export class EcosystemRepository { * @returns users list */ -async findEcosystemMembers( - ecosystemId: string, - pageNumber: number, - pageSize: number, - search: string, - sortBy: string -): Promise { - try { - const result = await this.prisma.$transaction([ - this.prisma.ecosystem_orgs.findMany({ - where: { - ecosystemId, - OR: [ - { - organisation: { - name: { contains: search, mode: 'insensitive' }, - // eslint-disable-next-line camelcase - org_agents: { - some: { - orgDid: { contains: search, mode: 'insensitive' } + async findEcosystemMembers( + ecosystemId: string, + pageNumber: number, + pageSize: number, + search: string, + sortBy: string, + sortField: string + ): Promise { + try { + const result = await this.prisma.$transaction([ + this.prisma.ecosystem_orgs.findMany({ + where: { + ecosystemId, + OR: [ + { + organisation: { + name: { contains: search, mode: 'insensitive' } + } + }, + { + organisation: { + // eslint-disable-next-line camelcase + org_agents: { + some: { + orgDid: { contains: search, mode: 'insensitive' } + } } } } + ] + }, + include: { + ecosystem: true, + ecosystemRole: true, + organisation: { + select: { + name: true, + orgSlug: true, + // eslint-disable-next-line camelcase + org_agents: true + } } - ] - }, - include: { - ecosystem: true, - ecosystemRole: true, - organisation: { - select: { - name: true, - orgSlug: true, - // eslint-disable-next-line camelcase - org_agents: true - } + }, + take: Number(pageSize), + skip: (pageNumber - 1) * pageSize, + orderBy: { + [sortField]: SortValue.ASC === sortBy ? 'asc' : 'desc' } - }, - take: Number(pageSize), - skip: (pageNumber - 1) * pageSize, - orderBy: { - createDateTime: 'asc' === sortBy ? 'asc' : 'desc' - } - }), - this.prisma.ecosystem_orgs.count({ - where: { - ecosystemId - } - }) - ]); - return result; - } catch (error) { - this.logger.error(`error: ${JSON.stringify(error)}`); - throw error; + }), + this.prisma.ecosystem_orgs.count({ + where: { + ecosystemId + } + }) + ]); + return result; + } catch (error) { + this.logger.error(`error: ${JSON.stringify(error)}`); + throw error; + } } -} async getEcosystemInvitationsPagination(queryObject: object, pageNumber: number, pageSize: number): Promise { try { @@ -590,18 +609,18 @@ async findEcosystemMembers( async fetchEcosystemOrg( payload: object ): Promise { - + return this.prisma.ecosystem_orgs.findFirst({ - where: { - ...payload - }, - select: { - ecosystem: true, - ecosystemRole: true, - organisation: true - } - }); - + where: { + ...payload + }, + select: { + ecosystem: true, + ecosystemRole: true, + organisation: true + } + }); + } @@ -660,20 +679,7 @@ async findEcosystemMembers( } - async getAllEcosystemSchemasDetails(payload: GetAllSchemaList): Promise<{ - schemasCount: number; - schemasResult: { - createDateTime: Date; - createdBy: string; - name: string; - version: string; - attributes: string; - schemaLedgerId: string; - publisherDid: string; - issuerId: string; - orgId: string; - }[]; - }> { + async getAllEcosystemSchemasDetails(payload: GetAllSchemaList): Promise { try { const { ecosystemId, search, pageNumber, pageSize } = payload; @@ -833,7 +839,7 @@ async findEcosystemMembers( schemaTransactionResponse: SchemaTransactionResponse, requestBody: object, type: endorsementTransactionType - // eslint-disable-next-line camelcase + // eslint-disable-next-line camelcase ): Promise { try { const { endorserDid, authorDid, requestPayload, status, ecosystemOrgId, userId } = schemaTransactionResponse; @@ -861,6 +867,18 @@ async findEcosystemMembers( // eslint-disable-next-line camelcase async deleteInvitations(invitationId: string): Promise { try { + + const findInvitation = await this.prisma.ecosystem_invitations.findUnique({ + where: { + id: invitationId, + status: EcosystemInvitationStatus.PENDING + } + }); + + if (!findInvitation) { + throw new NotFoundException('Ecosystem Invitation not found'); + } + const deletedInvitation = await this.prisma.ecosystem_invitations.delete({ where: { id: invitationId, @@ -898,7 +916,7 @@ async findEcosystemMembers( // eslint-disable-next-line camelcase async getEndorsementTransactionById(endorsementId: string, status: endorsementTransactionStatus): Promise { try { - const ecosystemLeadDetails = await this.prisma.endorsement_transaction.findFirst({ + const ecosystemLeadDetails = await this.prisma.endorsement_transaction.findUnique({ where: { id: endorsementId, status @@ -1029,6 +1047,26 @@ async findEcosystemMembers( } } + async schemaExist(schemaName: string, schemaVersion: string): Promise { + try { + return this.prisma.schema.findMany({ + where: { + name: { + contains: schemaName, + mode: 'insensitive' + }, + version: { + contains: schemaVersion, + mode: 'insensitive' + } + } + }); + } catch (error) { + this.logger.error(`Error in schemaExists: ${error}`); + throw error; + } + } + // eslint-disable-next-line camelcase async saveCredDef(credDefResult: saveCredDef): Promise { try { diff --git a/apps/ecosystem/src/ecosystem.service.ts b/apps/ecosystem/src/ecosystem.service.ts index 10236182f..9aa3dff26 100644 --- a/apps/ecosystem/src/ecosystem.service.ts +++ b/apps/ecosystem/src/ecosystem.service.ts @@ -30,15 +30,46 @@ import { } from '../enums/ecosystem.enum'; import { FetchInvitationsPayload } from '../interfaces/invitations.interface'; import { EcosystemMembersPayload } from '../interfaces/ecosystemMembers.interface'; -import { CreateEcosystem, CredDefMessage, IEcosystemDashboard, LedgerDetails, OrganizationData, RequestCredDeffEndorsement, RequestSchemaEndorsement, SaveSchema, SchemaMessage, SignedTransactionMessage, TransactionPayload, saveCredDef, submitTransactionPayload, IEcosystem, EcosystemDetailsResult, IEcosystemInvitation, IEcosystemInvitations, IEditEcosystem, IEndorsementTransaction } from '../interfaces/ecosystem.interfaces'; -import { GetAllSchemaList, GetEndorsementsPayload } from '../interfaces/endorsements.interface'; +import { + CreateEcosystem, + CredDefMessage, + IEcosystemDashboard, + LedgerDetails, + OrganizationData, + RequestCredDeffEndorsement, + RequestSchemaEndorsement, + SaveSchema, + SchemaMessage, + SignedTransactionMessage, + TransactionPayload, + saveCredDef, + submitTransactionPayload, + IEcosystem, + IEcosystemInvitation, + IEcosystemInvitations, + IEditEcosystem, + IEndorsementTransaction, + IEcosystemList +} from '../interfaces/ecosystem.interfaces'; +import { GetAllSchemaList, GetEndorsementsPayload, ISchemasResponse } from '../interfaces/endorsements.interface'; import { CommonConstants } from '@credebl/common/common.constant'; // eslint-disable-next-line camelcase -import { credential_definition, endorsement_transaction, org_agents, platform_config, schema, user } from '@prisma/client'; +import { + // eslint-disable-next-line camelcase + credential_definition, + // eslint-disable-next-line camelcase + endorsement_transaction, + // eslint-disable-next-line camelcase + org_agents, + // eslint-disable-next-line camelcase + platform_config, + schema, + user +} from '@prisma/client'; import { Cache } from 'cache-manager'; import { CACHE_MANAGER } from '@nestjs/cache-manager'; import { updateEcosystemOrgsDto } from '../dtos/update-ecosystemOrgs.dto'; - +import { IEcosystemDetails } from '@credebl/common/interfaces/ecosystem.interface'; @Injectable() export class EcosystemService { @@ -48,7 +79,7 @@ export class EcosystemService { private readonly logger: Logger, private readonly prisma: PrismaService, @Inject(CACHE_MANAGER) private cacheService: Cache - ) { } + ) {} /** * @@ -59,50 +90,52 @@ export class EcosystemService { // eslint-disable-next-line camelcase async createEcosystem(createEcosystemDto: CreateEcosystem): Promise { try { + const ecosystemExist = await this.ecosystemRepository.checkEcosystemNameExist(createEcosystemDto.name); - const ecosystemExist = await this.ecosystemRepository.checkEcosystemNameExist(createEcosystemDto.name); - - if (ecosystemExist) { - throw new ConflictException(ResponseMessages.ecosystem.error.exists); - } + if (ecosystemExist) { + throw new ConflictException(ResponseMessages.ecosystem.error.exists); + } - const isMultiEcosystemEnabled = await this.ecosystemRepository.getSpecificEcosystemConfig( - EcosystemConfigSettings.MULTI_ECOSYSTEM - ); + const isMultiEcosystemEnabled = await this.ecosystemRepository.getSpecificEcosystemConfig( + EcosystemConfigSettings.MULTI_ECOSYSTEM + ); - if (isMultiEcosystemEnabled && 'false' === isMultiEcosystemEnabled.value) { - const ecoOrganizationList = await this.ecosystemRepository.checkEcosystemOrgs(createEcosystemDto.orgId); + if (isMultiEcosystemEnabled && 'false' === isMultiEcosystemEnabled.value) { + const ecoOrganizationList = await this.ecosystemRepository.checkEcosystemOrgs(createEcosystemDto.orgId); - for (const organization of ecoOrganizationList) { - if (organization['ecosystemRole']['name'] === EcosystemRoles.ECOSYSTEM_MEMBER) { - throw new ConflictException(ResponseMessages.ecosystem.error.ecosystemOrgAlready); + for (const organization of ecoOrganizationList) { + if (organization['ecosystemRole']['name'] === EcosystemRoles.ECOSYSTEM_MEMBER) { + throw new ConflictException(ResponseMessages.ecosystem.error.ecosystemOrgAlready); + } } } - } - const orgDetails: OrganizationData = await this.getOrganizationDetails(createEcosystemDto.orgId, createEcosystemDto.userId); + const orgDetails: OrganizationData = await this.getOrganizationDetails( + createEcosystemDto.orgId, + createEcosystemDto.userId + ); - if (!orgDetails) { - throw new NotFoundException(ResponseMessages.ecosystem.error.orgNotExist); - } + if (!orgDetails) { + throw new NotFoundException(ResponseMessages.ecosystem.error.orgNotExist); + } - if (0 === orgDetails.org_agents.length) { - throw new NotFoundException(ResponseMessages.ecosystem.error.orgDidNotExist); - } + if (0 === orgDetails.org_agents.length) { + throw new NotFoundException(ResponseMessages.ecosystem.error.orgDidNotExist); + } - const ecosystemLedgers = orgDetails.org_agents.map((agent) => agent.ledgers.id); + const ecosystemLedgers = orgDetails.org_agents.map((agent) => agent.ledgers.id); - const createEcosystem = await this.ecosystemRepository.createNewEcosystem(createEcosystemDto, ecosystemLedgers); - if (!createEcosystem) { - throw new NotFoundException(ResponseMessages.ecosystem.error.notCreated); - } - - return createEcosystem; - } catch (error) { - this.logger.error(`createEcosystem: ${error}`); + const createEcosystem = await this.ecosystemRepository.createNewEcosystem(createEcosystemDto, ecosystemLedgers); + if (!createEcosystem) { + throw new NotFoundException(ResponseMessages.ecosystem.error.notCreated); + } + + return createEcosystem; + } catch (error) { + this.logger.error(`createEcosystem: ${error}`); throw new RpcException(error.response ? error.response : error); + } } -} async getOrganizationDetails(orgId: string, userId: string): Promise { const pattern = { cmd: 'get-organization-by-id' }; @@ -125,7 +158,6 @@ export class EcosystemService { return orgData; } - /** * * @param editEcosystemDto @@ -135,46 +167,55 @@ export class EcosystemService { // eslint-disable-next-line camelcase async editEcosystem(editEcosystemDto: CreateEcosystem, ecosystemId: string): Promise { try { - const { name, description, tags, logo, autoEndorsement, userId } = editEcosystemDto; + const { name, description, tags, logo, autoEndorsement, userId } = editEcosystemDto; - const updateData: CreateEcosystem = { - lastChangedBy: userId - }; + const updateData: CreateEcosystem = { + lastChangedBy: userId + }; - if (name) { updateData.name = name; } + if (name) { + updateData.name = name; + } - if (description) { updateData.description = description; } + if (description) { + updateData.description = description; + } - if (tags) { updateData.tags = tags; } + if (tags) { + updateData.tags = tags; + } - if (logo) { updateData.logoUrl = logo; } + if (logo) { + updateData.logoUrl = logo; + } - if ('' !== autoEndorsement.toString()) { updateData.autoEndorsement = autoEndorsement; } + if ('' !== autoEndorsement.toString()) { + updateData.autoEndorsement = autoEndorsement; + } - const ecosystemExist = await this.ecosystemRepository.checkEcosystemExist(editEcosystemDto.name, ecosystemId); + const ecosystemExist = await this.ecosystemRepository.checkEcosystemExist(editEcosystemDto.name, ecosystemId); - if (0 === ecosystemExist.length) { - const ecosystemExist = await this.ecosystemRepository.checkEcosystemNameExist(editEcosystemDto.name); - if (ecosystemExist) { - throw new ConflictException(ResponseMessages.ecosystem.error.exists); + if (0 === ecosystemExist.length) { + const ecosystemExist = await this.ecosystemRepository.checkEcosystemNameExist(editEcosystemDto.name); + if (ecosystemExist) { + throw new ConflictException(ResponseMessages.ecosystem.error.exists); + } } - } - const editEcosystem = await this.ecosystemRepository.updateEcosystemById(updateData, ecosystemId); - if (!editEcosystem) { - throw new NotFoundException(ResponseMessages.ecosystem.error.update); - } + const editEcosystem = await this.ecosystemRepository.updateEcosystemById(updateData, ecosystemId); + if (!editEcosystem) { + throw new NotFoundException(ResponseMessages.ecosystem.error.update); + } - // Removed unnecessary key from object - delete editEcosystem.deletedAt; + // Removed unnecessary key from object + delete editEcosystem.deletedAt; - return editEcosystem; - } catch (error) { + return editEcosystem; + } catch (error) { this.logger.error(`In update ecosystem : ${JSON.stringify(error)}`); throw new RpcException(error.response ? error.response : error); } } - /** * @@ -182,15 +223,32 @@ export class EcosystemService { * @returns all ecosystem details */ - // eslint-disable-next-line camelcase - async getAllEcosystem(payload: { orgId: string }): Promise { - const getAllEcosystemDetails = await this.ecosystemRepository.getAllEcosystemDetails(payload.orgId); + async getAllEcosystem(payload: IEcosystemList): Promise { + try { + const { orgId, pageNumber, pageSize, search } = payload; + + const getEcosystemOrgs = await this.ecosystemRepository.getAllEcosystemDetails( + orgId, + pageNumber, + pageSize, + search + ); + + const ecosystemListDetails = { + totalItems: getEcosystemOrgs[1], + hasNextPage: payload.pageSize * payload.pageNumber < getEcosystemOrgs[1], + hasPreviousPage: 1 < payload.pageNumber, + nextPage: Number(payload.pageNumber) + 1, + previousPage: payload.pageNumber - 1, + lastPage: Math.ceil(getEcosystemOrgs[1] / payload.pageSize), + ecosystemList: getEcosystemOrgs[0] + }; - if (!getAllEcosystemDetails) { - throw new NotFoundException(ResponseMessages.ecosystem.error.update); + return ecosystemListDetails; + } catch (error) { + this.logger.error(`In fetch ecosystem list : ${JSON.stringify(error)}`); + throw new RpcException(error.response ? error.response : error); } - - return getAllEcosystemDetails; } /** @@ -221,7 +279,7 @@ export class EcosystemService { config: endorseMemberCount.ecosystemConfigData } }; - + return dashboardDetails; } catch (error) { this.logger.error(`In ecosystem dashboard details : ${JSON.stringify(error)}`); @@ -248,7 +306,6 @@ export class EcosystemService { }); } - /** * Description: get an ecosystem invitation * @returns Get sent ecosystem invitation details @@ -267,10 +324,13 @@ export class EcosystemService { AND: [{ email: userEmail }, { status: { contains: search, mode: 'insensitive' } }] }; - const ecosystemInvitations = await this.ecosystemRepository.getEcosystemInvitationsPagination(query, pageNumber, pageSize); + const ecosystemInvitations = await this.ecosystemRepository.getEcosystemInvitationsPagination( + query, + pageNumber, + pageSize + ); for (const invitation of ecosystemInvitations.invitations) { - const ledgerNetworks = invitation.ecosystem.ledgers; const ledgerData = []; @@ -282,11 +342,9 @@ export class EcosystemService { } invitation.ecosystem.networkDetails = ledgerData; } - } return ecosystemInvitations; - } catch (error) { this.logger.error(`In error getEcosystemInvitations: ${JSON.stringify(error)}`); throw new InternalServerErrorException(error); @@ -299,16 +357,21 @@ export class EcosystemService { * @param userId * @returns */ - async createInvitation(bulkInvitationDto: BulkSendInvitationDto, userId: string, userEmail: string, orgId: string): Promise { + async createInvitation( + bulkInvitationDto: BulkSendInvitationDto, + userId: string, + userEmail: string, + orgId: string + ): Promise { const { invitations, ecosystemId } = bulkInvitationDto; const invitationResponse = []; try { const ecosystemDetails = await this.ecosystemRepository.getEcosystemDetails(ecosystemId); - if (!ecosystemDetails.ledgers - || (Array.isArray(ecosystemDetails.ledgers) - && 0 === ecosystemDetails.ledgers.length)) { - + if ( + !ecosystemDetails.ledgers || + (Array.isArray(ecosystemDetails.ledgers) && 0 === ecosystemDetails.ledgers.length) + ) { const ecosystemLeadDetails = await this.ecosystemRepository.getEcosystemLeadDetails(ecosystemId); const ecosystemAgents = await this.ecosystemRepository.getAllAgentDetails(ecosystemLeadDetails.orgId); @@ -380,7 +443,7 @@ export class EcosystemService { * @param userId * @returns Ecosystem invitation status */ - async acceptRejectEcosystemInvitations(acceptRejectInvitation: AcceptRejectEcosystemInvitationDto): Promise { + async acceptRejectEcosystemInvitations(acceptRejectInvitation: AcceptRejectEcosystemInvitationDto, email: string): Promise { try { const isMultiEcosystemEnabled = await this.ecosystemRepository.getSpecificEcosystemConfig( EcosystemConfigSettings.MULTI_ECOSYSTEM @@ -397,13 +460,15 @@ export class EcosystemService { } const { orgId, status, invitationId, orgName, orgDid, userId } = acceptRejectInvitation; - const invitation = await this.ecosystemRepository.getEcosystemInvitationById(invitationId); - + const invitation = await this.ecosystemRepository.getEcosystemInvitationById(invitationId, email); if (!invitation) { throw new NotFoundException(ResponseMessages.ecosystem.error.invitationNotFound); } - const orgDetails: OrganizationData = await this.getOrganizationDetails(acceptRejectInvitation.orgId, acceptRejectInvitation.userId); + const orgDetails: OrganizationData = await this.getOrganizationDetails( + acceptRejectInvitation.orgId, + acceptRejectInvitation.userId + ); if (!orgDetails) { throw new NotFoundException(ResponseMessages.ecosystem.error.orgNotExist); @@ -533,16 +598,28 @@ export class EcosystemService { * @param ecosystemName * @returns Send invitation mail */ - async sendInviteEmailTemplate(email: string, ecosystemName: string, firstName:string, orgName:string, isUserExist: boolean): Promise { + async sendInviteEmailTemplate( + email: string, + ecosystemName: string, + firstName: string, + orgName: string, + isUserExist: boolean + ): Promise { const platformConfigData = await this.prisma.platform_config.findMany(); const urlEmailTemplate = new EcosystemInviteTemplate(); const emailData = new EmailDto(); emailData.emailFrom = platformConfigData[0].emailFrom; emailData.emailTo = email; - emailData.emailSubject = `Invitation to join an Ecosystem “${ecosystemName}” on CREDEBL`; - - emailData.emailHtml = await urlEmailTemplate.sendInviteEmailTemplate(email, ecosystemName, firstName, orgName, isUserExist); + emailData.emailSubject = `Invitation to join an Ecosystem “${ecosystemName}” on ${process.env.PLATFORM_NAME}`; + + emailData.emailHtml = await urlEmailTemplate.sendInviteEmailTemplate( + email, + ecosystemName, + firstName, + orgName, + isUserExist + ); //Email is sent to user for the verification through emailData const isEmailSent = await sendEmail(emailData); @@ -574,12 +651,11 @@ export class EcosystemService { return false; } - async getEcoUserName(userEmail: string): Promise { const pattern = { cmd: 'get-user-by-mail' }; const payload = { email: userEmail }; - const userData = await this.ecosystemServiceProxy + const userData = await this.ecosystemServiceProxy .send(pattern, payload) .toPromise() .catch((error) => { @@ -591,9 +667,9 @@ export class EcosystemService { }, error.status ); - }); - return userData; - } + }); + return userData; + } // eslint-disable-next-line camelcase async removeEndorsementTransactionFields(transactionObject: endorsement_transaction): Promise { @@ -620,9 +696,15 @@ export class EcosystemService { ecosystemId: string ): Promise { try { - const getEcosystemLeadDetails = await this.ecosystemRepository.getEcosystemLeadDetails(ecosystemId); - const { name, version } = requestSchemaPayload; + const alreadySchemaExist = await this._schemaExist(version, name); + this.logger.log(`alreadySchemaExist ::: ${JSON.stringify(alreadySchemaExist.length)}`); + + if (0 !== alreadySchemaExist.length) { + throw new ConflictException(ResponseMessages.ecosystem.error.schemaAlreadyExist); + } + + const getEcosystemLeadDetails = await this.ecosystemRepository.getEcosystemLeadDetails(ecosystemId); if (0 === name.length) { throw new BadRequestException(ResponseMessages.schema.error.nameNotEmpty); @@ -634,17 +716,17 @@ export class EcosystemService { const schemaVersionIndexOf = -1; - if ( - isNaN(parseFloat(version)) || - version.toString().indexOf('.') === - schemaVersionIndexOf - ) { - throw new NotAcceptableException( - ResponseMessages.schema.error.invalidVersion - ); + if (isNaN(parseFloat(version)) || version.toString().indexOf('.') === schemaVersionIndexOf) { + throw new NotAcceptableException(ResponseMessages.schema.error.invalidVersion); } - const [schemaRequestExist, ecosystemMemberDetails, platformConfig, ecosystemLeadAgentDetails, getEcosystemOrgDetailsByOrgId] = await Promise.all([ + const [ + schemaRequestExist, + ecosystemMemberDetails, + platformConfig, + ecosystemLeadAgentDetails, + getEcosystemOrgDetailsByOrgId + ] = await Promise.all([ this.ecosystemRepository.findRecordsByNameAndVersion(requestSchemaPayload?.name, requestSchemaPayload?.version), this.ecosystemRepository.getAgentDetails(orgId), this.ecosystemRepository.getPlatformConfigDetails(), @@ -652,10 +734,12 @@ export class EcosystemService { this.ecosystemRepository.getEcosystemOrgDetailsbyId(orgId, ecosystemId) ]); - const existSchema = schemaRequestExist?.filter(schema => schema.status === endorsementTransactionStatus.REQUESTED || - schema.status === endorsementTransactionStatus.SIGNED || - schema.status === endorsementTransactionStatus.SUBMITED -) ?? []; + const existSchema = + schemaRequestExist?.filter( + (schema) => schema.status === endorsementTransactionStatus.REQUESTED || + schema.status === endorsementTransactionStatus.SIGNED || + schema.status === endorsementTransactionStatus.SUBMITED + ) ?? []; if (0 < existSchema.length) { throw new ConflictException(ResponseMessages.ecosystem.error.schemaAlreadyExist); @@ -688,7 +772,6 @@ export class EcosystemService { endorsementTransactionType.SCHEMA, ecosystemMemberDetails.tenantId ); - const apiKey = await this._getOrgAgentApiKey(orgId); const attributeArray = requestSchemaPayload.attributes.map((item) => item.attributeName); const schemaTransactionPayload = { @@ -703,7 +786,7 @@ export class EcosystemService { const schemaTransactionRequest: SchemaMessage = await this._requestSchemaEndorsement( schemaTransactionPayload, url, - apiKey + orgId ); const schemaTransactionResponse = { @@ -734,9 +817,7 @@ export class EcosystemService { const errorObj = error?.status?.message?.error; if (errorObj) { throw new RpcException({ - message: errorObj?.reason - ? errorObj?.reason - : errorObj, + message: errorObj?.reason ? errorObj?.reason : errorObj, statusCode: error?.status?.code }); } else { @@ -745,6 +826,26 @@ export class EcosystemService { } } + async _schemaExist(version: string, schemaName: string): Promise { + const pattern = { cmd: 'schema-exist' }; + const payload = { version, schemaName }; + + try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const message = await this.ecosystemServiceProxy.send(pattern, payload).toPromise(); + return message; + } catch (error) { + this.logger.error(`catch: ${JSON.stringify(error)}`); + throw new HttpException( + { + status: error.status, + error: error.message + }, + error.status + ); + } + } + async requestCredDeffEndorsement( requestCredDefPayload: RequestCredDeffEndorsement, orgId: string, @@ -767,7 +868,14 @@ export class EcosystemService { this.ecosystemRepository.getEcosystemOrgDetailsbyId(orgId, ecosystemId) ]); - if (0 !== credDefRequestExist.length) { + const existsCredDef = + credDefRequestExist?.filter( + (tag) => tag.status === endorsementTransactionStatus.REQUESTED || + tag.status === endorsementTransactionStatus.SIGNED || + tag.status === endorsementTransactionStatus.SUBMITED + ) ?? []; + + if (0 < existsCredDef.length) { throw new ConflictException(ResponseMessages.ecosystem.error.credDefAlreadyExist); } @@ -791,66 +899,91 @@ export class EcosystemService { throw new NotFoundException(ResponseMessages.ecosystem.error.ecosystemOrgNotFound); } - const orgAgentType = await this.ecosystemRepository.getOrgAgentType(ecosystemMemberDetails.orgAgentTypeId); - const url = await this.getAgentUrl( - orgAgentType, - ecosystemMemberDetails.agentEndPoint, - endorsementTransactionType.CREDENTIAL_DEFINITION, - ecosystemMemberDetails.tenantId - ); - const apiKey = await this._getOrgAgentApiKey(orgId); - const credDefTransactionPayload = { - endorserDid: ecosystemLeadAgentDetails.orgDid, - endorse: requestCredDefPayload.endorse, - tag: requestCredDefPayload.tag, - schemaId: requestCredDefPayload.schemaId, - issuerId: ecosystemMemberDetails.orgDid - }; + let requestCredDefBody; + const credDefData = credDefRequestExist?.filter((tag) => tag.status === endorsementTransactionStatus.DECLINED); + if (0 < credDefData.length) { + let schemaTransactionResponse; + credDefRequestExist.forEach((tag) => { + requestCredDefBody = tag.requestBody; + schemaTransactionResponse = { + endorserDid: ecosystemLeadAgentDetails.orgDid, + authorDid: ecosystemMemberDetails.orgDid, + requestPayload: tag.requestPayload, + status: endorsementTransactionStatus.REQUESTED, + ecosystemOrgId: getEcosystemOrgDetailsByOrgId.id, + userId: requestCredDefPayload.userId + }; + }); + const storeTransaction = await this.ecosystemRepository.storeTransactionRequest( + schemaTransactionResponse, + requestCredDefBody, + endorsementTransactionType.CREDENTIAL_DEFINITION + ); - const credDefTransactionRequest: CredDefMessage = await this._requestCredDeffEndorsement( - credDefTransactionPayload, - url, - apiKey - ); + // To return selective response + await this.removeEndorsementTransactionFields(storeTransaction); - if ('failed' === credDefTransactionRequest.message.credentialDefinitionState.state) { - throw new InternalServerErrorException(ResponseMessages.ecosystem.error.requestCredDefTransaction); - } + await new Promise((resolve) => setTimeout(resolve, 5000)); + return storeTransaction; + } else { + const orgAgentType = await this.ecosystemRepository.getOrgAgentType(ecosystemMemberDetails.orgAgentTypeId); + const url = await this.getAgentUrl( + orgAgentType, + ecosystemMemberDetails.agentEndPoint, + endorsementTransactionType.CREDENTIAL_DEFINITION, + ecosystemMemberDetails.tenantId + ); + const credDefTransactionPayload = { + endorserDid: ecosystemLeadAgentDetails.orgDid, + endorse: requestCredDefPayload.endorse, + tag: requestCredDefPayload.tag, + schemaId: requestCredDefPayload.schemaId, + issuerId: ecosystemMemberDetails.orgDid + }; - const requestBody = credDefTransactionRequest.message.credentialDefinitionState.credentialDefinition; + const credDefTransactionRequest: CredDefMessage = await this._requestCredDeffEndorsement( + credDefTransactionPayload, + url, + orgId + ); - if (!requestBody) { - throw new NotFoundException(ResponseMessages.ecosystem.error.credentialDefinitionNotFound); - } + if ('failed' === credDefTransactionRequest.message.credentialDefinitionState.state) { + throw new InternalServerErrorException(ResponseMessages.ecosystem.error.requestCredDefTransaction); + } - requestCredDefPayload['credentialDefinition'] = requestBody; - const schemaTransactionResponse = { - endorserDid: ecosystemLeadAgentDetails.orgDid, - authorDid: ecosystemMemberDetails.orgDid, - requestPayload: credDefTransactionRequest.message.credentialDefinitionState.credentialDefinitionRequest, - status: endorsementTransactionStatus.REQUESTED, - ecosystemOrgId: getEcosystemOrgDetailsByOrgId.id, - userId: requestCredDefPayload.userId - }; + const requestBody = credDefTransactionRequest.message.credentialDefinitionState.credentialDefinition; - const storeTransaction = await this.ecosystemRepository.storeTransactionRequest( - schemaTransactionResponse, - requestCredDefPayload, - endorsementTransactionType.CREDENTIAL_DEFINITION - ); + if (!requestBody) { + throw new NotFoundException(ResponseMessages.ecosystem.error.credentialDefinitionNotFound); + } - // To return selective response - await this.removeEndorsementTransactionFields(storeTransaction); + requestCredDefPayload['credentialDefinition'] = requestBody; + const schemaTransactionResponse = { + endorserDid: ecosystemLeadAgentDetails.orgDid, + authorDid: ecosystemMemberDetails.orgDid, + requestPayload: credDefTransactionRequest.message.credentialDefinitionState.credentialDefinitionRequest, + status: endorsementTransactionStatus.REQUESTED, + ecosystemOrgId: getEcosystemOrgDetailsByOrgId.id, + userId: requestCredDefPayload.userId + }; - return storeTransaction; + const storeTransaction = await this.ecosystemRepository.storeTransactionRequest( + schemaTransactionResponse, + requestCredDefPayload, + endorsementTransactionType.CREDENTIAL_DEFINITION + ); + + // To return selective response + await this.removeEndorsementTransactionFields(storeTransaction); + + return storeTransaction; + } } catch (error) { this.logger.error(`In request cred-def endorsement: ${JSON.stringify(error)}`); const errorObj = error?.status?.message?.error; if (errorObj) { throw new RpcException({ - message: errorObj?.reason - ? errorObj?.reason - : errorObj, + message: errorObj?.reason ? errorObj?.reason : errorObj, statusCode: error?.status?.code }); } else { @@ -875,9 +1008,9 @@ export class EcosystemService { } } - async _requestSchemaEndorsement(requestSchemaPayload: object, url: string, apiKey: string): Promise { + async _requestSchemaEndorsement(requestSchemaPayload: object, url: string, orgId: string): Promise { const pattern = { cmd: 'agent-schema-endorsement-request' }; - const payload = { requestSchemaPayload, url, apiKey }; + const payload = { requestSchemaPayload, url, orgId }; try { // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -895,9 +1028,9 @@ export class EcosystemService { } } - async _requestCredDeffEndorsement(requestSchemaPayload: object, url: string, apiKey: string): Promise { + async _requestCredDeffEndorsement(requestSchemaPayload: object, url: string, orgId: string): Promise { const pattern = { cmd: 'agent-credDef-endorsement-request' }; - const payload = { requestSchemaPayload, url, apiKey }; + const payload = { requestSchemaPayload, url, orgId }; try { // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -948,17 +1081,14 @@ export class EcosystemService { endorsementTransactionType.SIGN, ecosystemLeadAgentDetails?.tenantId ); - let apiKey: string = await this.cacheService.get(CommonConstants.CACHE_APIKEY_KEY); - if (!apiKey || null === apiKey || undefined === apiKey) { - apiKey = await this._getOrgAgentApiKey(ecosystemLeadDetails.orgId); - } + const jsonString = endorsementTransactionPayload.requestPayload.toString(); const payload = { transaction: jsonString, endorserDid: endorsementTransactionPayload.endorserDid }; - const schemaTransactionRequest: SignedTransactionMessage = await this._signTransaction(payload, url, apiKey); + const schemaTransactionRequest: SignedTransactionMessage = await this._signTransaction(payload, url, ecosystemLeadDetails.orgId); if (!schemaTransactionRequest) { throw new InternalServerErrorException(ResponseMessages.ecosystem.error.signRequestError); @@ -989,13 +1119,13 @@ export class EcosystemService { if (!submitTxn) { await this.ecosystemRepository.updateTransactionStatus(endorsementId, endorsementTransactionStatus.REQUESTED); throw new InternalServerErrorException(ResponseMessages.ecosystem.error.sumbitTransaction); - } + } return { - autoEndorsement:ecosystemDetails.autoEndorsement, + autoEndorsement: ecosystemDetails.autoEndorsement, submitTxn - }; + }; } - + // To return selective response await this.removeEndorsementTransactionFields(updateSignedTransaction); @@ -1020,15 +1150,21 @@ export class EcosystemService { * @returns Ecosystem members list */ - async getEcoystemMembers(payload: EcosystemMembersPayload): Promise { + async getEcosystemMembers(payload: EcosystemMembersPayload): Promise { try { - const { ecosystemId, pageNumber, pageSize, search, sortBy } = payload; - const getEcosystemMember = await this.ecosystemRepository.findEcosystemMembers(ecosystemId, pageNumber, pageSize, search, sortBy); - + const { ecosystemId, pageNumber, pageSize, search, sortBy, sortField } = payload; + const getEcosystemMember = await this.ecosystemRepository.findEcosystemMembers( + ecosystemId, + pageNumber, + pageSize, + search, + sortBy, + sortField + ); + const ecosystemMemberResponse = { totalItems: getEcosystemMember[1], - hasNextPage: - payload.pageSize * payload.pageNumber < getEcosystemMember[1], + hasNextPage: payload.pageSize * payload.pageNumber < getEcosystemMember[1], hasPreviousPage: 1 < payload.pageNumber, nextPage: Number(payload.pageNumber) + 1, previousPage: payload.pageNumber - 1, @@ -1057,9 +1193,9 @@ export class EcosystemService { * @param url * @returns sign message */ - async _signTransaction(signEndorsementPayload: object, url: string, apiKey: string): Promise { + async _signTransaction(signEndorsementPayload: object, url: string, orgId: string): Promise { const pattern = { cmd: 'agent-sign-transaction' }; - const payload = { signEndorsementPayload, url, apiKey }; + const payload = { signEndorsementPayload, url, orgId }; try { // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -1197,11 +1333,12 @@ export class EcosystemService { async submitTransaction(transactionPayload: TransactionPayload): Promise { try { - const { endorsementId, ecosystemId, ecosystemLeadAgentEndPoint, orgId } = transactionPayload; + const { endorsementId, ecosystemId, ecosystemLeadAgentEndPoint, orgId } = transactionPayload; const endorsementTransactionPayload = await this.ecosystemRepository.getEndorsementTransactionById( endorsementId, endorsementTransactionStatus.SIGNED ); + if (!endorsementTransactionPayload) { throw new InternalServerErrorException(ResponseMessages.ecosystem.error.invalidTransaction); } @@ -1229,13 +1366,23 @@ export class EcosystemService { ecosystemMemberDetails, ecosystemLeadAgentDetails ); - - let apiKey: string = await this.cacheService.get(CommonConstants.CACHE_APIKEY_KEY); - if (!apiKey || null === apiKey || undefined === apiKey) { - apiKey = await this._getOrgAgentApiKey(orgId); + + if (endorsementTransactionPayload.type === endorsementTransactionType.SCHEMA) { + const isSchemaExists = await this.ecosystemRepository.schemaExist( + payload.schema.name, + payload.schema.version + ); + + if (0 !== isSchemaExists.length) { + this.logger.error(ResponseMessages.ecosystem.error.schemaAlreadyExist); + throw new ConflictException( + ResponseMessages.ecosystem.error.schemaAlreadyExist, + { cause: new Error(), description: ResponseMessages.errorMessages.conflict } + ); + } } - const submitTransactionRequest = await this._submitTransaction(payload, url, apiKey); + const submitTransactionRequest = await this._submitTransaction(payload, url, orgId); if ('failed' === submitTransactionRequest['message'].state) { throw new InternalServerErrorException(ResponseMessages.ecosystem.error.sumbitTransaction); @@ -1283,7 +1430,7 @@ export class EcosystemService { throw new InternalServerErrorException(ResponseMessages.ecosystem.error.updateCredDefId); } return this.handleCredDefSubmission( - endorsementTransactionPayload, + endorsementTransactionPayload, ecosystemMemberDetails, submitTransactionRequest ); @@ -1309,20 +1456,21 @@ export class EcosystemService { * @param url * @returns sign message */ - async _submitTransaction(submitEndorsementPayload: object, url: string, apiKey: string): Promise { + async _submitTransaction(submitEndorsementPayload: object, url: string, orgId: string): Promise { const pattern = { cmd: 'agent-submit-transaction' }; - const payload = { submitEndorsementPayload, url, apiKey }; + const payload = { submitEndorsementPayload, url, orgId }; try { // eslint-disable-next-line @typescript-eslint/no-explicit-any const message = await this.ecosystemServiceProxy.send(pattern, payload).toPromise(); return { message }; } catch (error) { - this.logger.error(`catch: ${JSON.stringify(error)}`); + this.logger.error(` agent-submit-transaction catch: ${JSON.stringify(error)}`); throw new HttpException( { status: error.status, - error: error.message + message: error.message, + error: error.error }, error.status ); @@ -1346,7 +1494,8 @@ export class EcosystemService { if (!resourceId) { throw new Error( - `${ResponseMessages.ecosystem.error.invalidTransactionMessage} Missing "${transactionType === endorsementTransactionType.SCHEMA ? 'schemaId' : 'credentialDefinitionId' + `${ResponseMessages.ecosystem.error.invalidTransactionMessage} Missing "${ + transactionType === endorsementTransactionType.SCHEMA ? 'schemaId' : 'credentialDefinitionId' }" property.` ); } @@ -1462,7 +1611,7 @@ export class EcosystemService { } } - async getAllEcosystemSchemas(ecosystemSchemas: GetAllSchemaList): Promise { + async getAllEcosystemSchemas(ecosystemSchemas: GetAllSchemaList): Promise { try { const response = await this.ecosystemRepository.getAllEcosystemSchemasDetails(ecosystemSchemas); const schemasDetails = response?.schemasResult.map((schemaAttributeItem) => { @@ -1470,15 +1619,11 @@ export class EcosystemService { return { ...schemaAttributeItem, attributes }; }); - const schemasResponse = { - totalItems: response.schemasCount, - hasNextPage: ecosystemSchemas.pageSize * ecosystemSchemas.pageNumber < response.schemasCount, - hasPreviousPage: 1 < ecosystemSchemas.pageNumber, - nextPage: ecosystemSchemas.pageNumber + 1, - previousPage: ecosystemSchemas.pageNumber - 1, - lastPage: Math.ceil(response.schemasCount / ecosystemSchemas.pageSize), - data: schemasDetails + const schemasResponse: ISchemasResponse = { + schemasCount: response.schemasCount, + schemasResult: schemasDetails }; + return schemasResponse; } catch (error) { this.logger.error(`In error fetching all ecosystem schemas: ${JSON.stringify(error)}`); @@ -1510,10 +1655,10 @@ export class EcosystemService { async declineEndorsementRequestByLead(ecosystemId: string, endorsementId: string): Promise { try { const declineResponse = await this.ecosystemRepository.updateEndorsementRequestStatus(ecosystemId, endorsementId); - + // To return selective response this.removeEndorsementTransactionFields(declineResponse); - + return declineResponse; } catch (error) { this.logger.error(`error in decline endorsement request: ${error}`); @@ -1521,7 +1666,6 @@ export class EcosystemService { } } - async _getOrgAgentApiKey(orgId: string): Promise { const pattern = { cmd: 'get-org-agent-api-key' }; const payload = { orgId }; @@ -1532,10 +1676,13 @@ export class EcosystemService { return message; } catch (error) { this.logger.error(`catch: ${JSON.stringify(error)}`); - throw new HttpException({ - status: error.status, - error: error.message - }, error.status); + throw new HttpException( + { + status: error.status, + error: error.message + }, + error.status + ); } } } diff --git a/apps/ecosystem/templates/EcosystemInviteTemplate.ts b/apps/ecosystem/templates/EcosystemInviteTemplate.ts index 4403e1785..2d1166ea0 100644 --- a/apps/ecosystem/templates/EcosystemInviteTemplate.ts +++ b/apps/ecosystem/templates/EcosystemInviteTemplate.ts @@ -12,13 +12,13 @@ export class EcosystemInviteTemplate { const message = isUserExist ? `Please accept the invitation using the following link:` - : `To get started, kindly register on CREDEBL platform using this link:`; + : `To get started, kindly register on ${process.env.PLATFORM_NAME} platform using this link:`; const secondMessage = isUserExist - ? `After successful login into CREDEBL and click on "Accept Ecosystem Invitation" link on your dashboard to start participating in the digital trust ecosystem.` - : `After successful registration, you can log into CREDEBL and click on "Accept Ecosystem Invitation" link on your dashboard to start participating in the digital trust ecosystem.`; + ? `After successful login into ${process.env.PLATFORM_NAME} and click on "Accept Ecosystem Invitation" link on your dashboard to start participating in the digital trust ecosystem.` + : `After successful registration, you can log into ${process.env.PLATFORM_NAME} and click on "Accept Ecosystem Invitation" link on your dashboard to start participating in the digital trust ecosystem.`; - const Button = isUserExist ? 'Accept Ecosystem Invitation' : 'Register on CREDEBL'; + const Button = isUserExist ? `Accept Ecosystem Invitation` : `Register on ${process.env.PLATFORM_NAME}`; return ` @@ -33,7 +33,7 @@ export class EcosystemInviteTemplate {
- CREDEBL logo + ${process.env.PLATFORM_NAME} logo
- For any assistance or questions while accessing your account, please do not hesitate to contact the support team at support@blockster.global. Our team will ensure a seamless onboarding experience for you. + For any assistance or questions while accessing your account, please do not hesitate to contact the support team at ${process.env.PUBLIC_PLATFORM_SUPPORT_EMAIL}. Our team will ensure a seamless onboarding experience for you.

- © Blockster Labs Pvt. Ltd. + © ${process.env.POWERED_BY}

diff --git a/apps/issuance/interfaces/issuance.interfaces.ts b/apps/issuance/interfaces/issuance.interfaces.ts index 3eeaacb74..5747d7a69 100644 --- a/apps/issuance/interfaces/issuance.interfaces.ts +++ b/apps/issuance/interfaces/issuance.interfaces.ts @@ -1,11 +1,15 @@ // eslint-disable-next-line camelcase import { AutoAccept } from '@credebl/enum/enum'; import { IUserRequest } from '@credebl/user-request/user-request.interface'; +import { organisation } from '@prisma/client'; import { IUserRequestInterface } from 'apps/agent-service/src/interface/agent-service.interface'; +import { IssueCredentialType } from 'apps/api-gateway/src/issuance/interfaces'; export interface IAttributes { + attributeName: string; name: string; value: string; + isRequired?: boolean; } export interface IIssuance { user?: IUserRequest; @@ -81,7 +85,8 @@ export interface IPattern { export interface ISendOfferNatsPayload { issueData: IIssueData, url: string, - apiKey: string; + apiKey?: string; + orgId?: string; } export interface IIssueCredentialsDefinitions { @@ -125,12 +130,22 @@ export interface ICredentialAttributesInterface { value: string; } +export interface ICredential{ + '@context':[]; + type: string[]; +} +export interface IOptions{ + proofType:string; + proofPurpose:string; +} export interface CredentialOffer { emailId: string; attributes: IAttributes[]; + credential?:ICredential; + options?:IOptions } export interface OutOfBandCredentialOfferPayload { - credentialDefinitionId: string; + credentialDefinitionId?: string; orgId: string; comment?: string; credentialOffer?: CredentialOffer[]; @@ -140,8 +155,10 @@ export interface OutOfBandCredentialOfferPayload { goalCode?: string, parentThreadId?: string, willConfirm?: boolean, - label?: string + label?: string, + imageUrl?: string, autoAcceptCredential?: string; + credentialType?:IssueCredentialType; } export interface OutOfBandCredentialOffer { @@ -188,10 +205,11 @@ export interface FileUploadData { jobId: string; } -export interface ClientDetails { +export interface IClientDetails { clientId: string; - userId?: string; + isSelectiveIssuance?: boolean; + fileName?: string; } export interface IIssuedCredentialsSearchInterface { issuedCredentialsSearchCriteria: IIssuedCredentialsSearchCriteria; @@ -203,6 +221,40 @@ export interface IIssuedCredentialsSearchCriteria { pageSize: number; sortField: string; sortBy: string; - searchByText: string; + search: string; user?: IUserRequestInterface; } + +export interface OrgAgent { + organisation: organisation; + id: string; + createDateTime: Date; + createdBy: string; + lastChangedDateTime: Date; + lastChangedBy: string; + orgDid: string; + verkey: string; + agentEndPoint: string; + agentId: string; + isDidPublic: boolean; + ledgerId: string; + orgAgentTypeId: string; + tenantId: string; +} + +export interface SendEmailCredentialOffer { + iterator: CredentialOffer; + emailId: string; + index: number; + credentialType: IssueCredentialType; + protocolVersion: string; + attributes: IAttributes[]; + credentialDefinitionId: string; + outOfBandCredential: OutOfBandCredentialOfferPayload; + comment: string; + organisation: organisation; + errors; + url: string; + orgId: string; + organizationDetails: organisation; +} \ No newline at end of file diff --git a/apps/issuance/src/issuance.controller.ts b/apps/issuance/src/issuance.controller.ts index 544212ac4..1c3d0cd85 100644 --- a/apps/issuance/src/issuance.controller.ts +++ b/apps/issuance/src/issuance.controller.ts @@ -1,6 +1,6 @@ import { Controller } from '@nestjs/common'; import { MessagePattern } from '@nestjs/microservices'; -import { ClientDetails, ICreateOfferResponse, IIssuance, IIssueCredentials, IIssueCredentialsDefinitions, ImportFileDetails, IssueCredentialWebhookPayload, OutOfBandCredentialOffer, PreviewRequest } from '../interfaces/issuance.interfaces'; +import { IClientDetails, ICreateOfferResponse, IIssuance, IIssueCredentials, IIssueCredentialsDefinitions, ImportFileDetails, IssueCredentialWebhookPayload, OutOfBandCredentialOffer, PreviewRequest } from '../interfaces/issuance.interfaces'; import { IssuanceService } from './issuance.service'; import { IIssuedCredential } from '@credebl/common/interfaces/issuance.interface'; import { OOBIssueCredentialDto } from 'apps/api-gateway/src/issuance/dtos/issuance.dto'; @@ -82,8 +82,8 @@ export class IssuanceController { @MessagePattern({ cmd: 'issue-bulk-credentials' }) - async issueBulkCredentials(payload: { requestId: string, orgId: string, clientDetails: ClientDetails }): Promise { - return this.issuanceService.issueBulkCredential(payload.requestId, payload.orgId, payload.clientDetails); + async issueBulkCredentials(payload: { requestId: string, orgId: string, clientDetails: IClientDetails, reqPayload: ImportFileDetails }): Promise { + return this.issuanceService.issueBulkCredential(payload.requestId, payload.orgId, payload.clientDetails, payload.reqPayload); } @MessagePattern({ cmd: 'retry-bulk-credentials' }) diff --git a/apps/issuance/src/issuance.repository.ts b/apps/issuance/src/issuance.repository.ts index c6078c128..1a6256ff4 100644 --- a/apps/issuance/src/issuance.repository.ts +++ b/apps/issuance/src/issuance.repository.ts @@ -15,6 +15,7 @@ import { ResponseMessages } from '@credebl/common/response-messages'; import { FileUploadData, IssueCredentialWebhookPayload, + OrgAgent, PreviewRequest, SchemaDetails } from '../interfaces/issuance.interfaces'; @@ -35,11 +36,14 @@ export class IssuanceRepository { * @returns Get getAgentEndPoint details */ // eslint-disable-next-line camelcase - async getAgentEndPoint(orgId: string): Promise { + async getAgentEndPoint(orgId: string): Promise { try { const agentDetails = await this.prisma.org_agents.findFirst({ where: { orgId + }, + include: { + organisation: true } }); @@ -67,6 +71,23 @@ export class IssuanceRepository { } } + + async getRecipientKeyByOrgId(orgId: string): Promise { + try { + return this.prisma.agent_invitations.findMany({ + where: { + orgId + }, + orderBy: { + createDateTime: 'asc' + } + }); + } catch (error) { + this.logger.error(`Error in getRecipientKey in issuance repository: ${error.message}`); + throw error; + } + } + async getAllIssuedCredentials( user: IUserRequest, orgId: string, @@ -87,11 +108,12 @@ export class IssuanceRepository { where: { orgId, OR: [ - { schemaId: { contains: issuedCredentialsSearchCriteria.searchByText, mode: 'insensitive' } }, - { connectionId: { contains: issuedCredentialsSearchCriteria.searchByText, mode: 'insensitive' } } + { schemaId: { contains: issuedCredentialsSearchCriteria.search, mode: 'insensitive' } }, + { connectionId: { contains: issuedCredentialsSearchCriteria.search, mode: 'insensitive' } } ] }, select: { + credentialExchangeId: true, createDateTime: true, createdBy: true, orgId: true, @@ -111,8 +133,8 @@ export class IssuanceRepository { where: { orgId, OR: [ - { schemaId: { contains: issuedCredentialsSearchCriteria.searchByText, mode: 'insensitive' } }, - { connectionId: { contains: issuedCredentialsSearchCriteria.searchByText, mode: 'insensitive' } } + { schemaId: { contains: issuedCredentialsSearchCriteria.search, mode: 'insensitive' } }, + { connectionId: { contains: issuedCredentialsSearchCriteria.search, mode: 'insensitive' } } ] } }); @@ -184,35 +206,6 @@ export class IssuanceRepository { } } - /** - * Description: Save connection details - * @param connectionInvitation - * @param agentId - * @param orgId - * @returns Get connection details - */ - // eslint-disable-next-line camelcase - async saveAgentConnectionInvitations( - connectionInvitation: string, - agentId: string, - orgId: string - ): Promise { - try { - const agentInvitationData = await this.prisma.agent_invitations.create({ - data: { - orgId, - agentId, - connectionInvitation, - multiUse: true - } - }); - return agentInvitationData; - } catch (error) { - this.logger.error(`Error in saveAgentConnectionInvitations: ${error.message} `); - throw error; - } - } - /** * Get platform config details * @returns diff --git a/apps/issuance/src/issuance.service.ts b/apps/issuance/src/issuance.service.ts index c38ccf305..92ae0a39f 100644 --- a/apps/issuance/src/issuance.service.ts +++ b/apps/issuance/src/issuance.service.ts @@ -1,7 +1,7 @@ /* eslint-disable no-useless-catch */ /* eslint-disable camelcase */ import { CommonService } from '@credebl/common'; -import { BadRequestException, HttpException, Inject, Injectable, InternalServerErrorException, Logger, NotFoundException } from '@nestjs/common'; +import { BadRequestException, ConflictException, HttpException, Inject, Injectable, InternalServerErrorException, Logger, NotFoundException } from '@nestjs/common'; import { IssuanceRepository } from './issuance.repository'; import { IUserRequest } from '@credebl/user-request/user-request.interface'; import { CommonConstants } from '@credebl/common/common.constant'; @@ -9,7 +9,7 @@ import { ResponseMessages } from '@credebl/common/response-messages'; import { ClientProxy, RpcException } from '@nestjs/microservices'; import { map } from 'rxjs'; // import { ClientDetails, FileUploadData, ICredentialAttributesInterface, ImportFileDetails, OutOfBandCredentialOfferPayload, PreviewRequest, SchemaDetails } from '../interfaces/issuance.interfaces'; -import { ClientDetails, FileUploadData, ICreateOfferResponse, IIssuance, IIssueData, IPattern, ISendOfferNatsPayload, ImportFileDetails, IssueCredentialWebhookPayload, OutOfBandCredentialOfferPayload, PreviewRequest, SchemaDetails } from '../interfaces/issuance.interfaces'; +import { CredentialOffer, FileUploadData, IAttributes, IClientDetails, ICreateOfferResponse, IIssuance, IIssueData, IPattern, ISendOfferNatsPayload, ImportFileDetails, IssueCredentialWebhookPayload, OutOfBandCredentialOfferPayload, PreviewRequest, SchemaDetails, SendEmailCredentialOffer } from '../interfaces/issuance.interfaces'; import { OrgAgentType } from '@credebl/enum/enum'; // import { platform_config } from '@prisma/client'; import * as QRCode from 'qrcode'; @@ -29,9 +29,10 @@ import { Queue } from 'bull'; import { FileUploadStatus, FileUploadType } from 'apps/api-gateway/src/enum'; import { AwsService } from '@credebl/aws'; import { io } from 'socket.io-client'; -import { IIssuedCredentialSearchParams } from 'apps/api-gateway/src/issuance/interfaces'; +import { IIssuedCredentialSearchParams, IssueCredentialType } from 'apps/api-gateway/src/issuance/interfaces'; import { IIssuedCredential } from '@credebl/common/interfaces/issuance.interface'; import { OOBIssueCredentialDto } from 'apps/api-gateway/src/issuance/dtos/issuance.dto'; +import { agent_invitations, organisation } from '@prisma/client'; @Injectable() @@ -51,8 +52,38 @@ export class IssuanceService { async sendCredentialCreateOffer(payload: IIssuance): Promise { + try { const { orgId, credentialDefinitionId, comment, connectionId, attributes } = payload || {}; + + const schemaResponse: SchemaDetails = await this.issuanceRepository.getCredentialDefinitionDetails( + credentialDefinitionId + ); + + if (schemaResponse?.attributes) { + const schemaResponseError = []; + const attributesArray: IAttributes[] = JSON.parse(schemaResponse.attributes); + + attributesArray.forEach((attribute) => { + if (attribute.attributeName && attribute.isRequired) { + + payload.attributes.map((attr) => { + if (attr.name === attribute.attributeName && attribute.isRequired && !attr.value) { + schemaResponseError.push( + `Attribute ${attribute.attributeName} is required` + ); + } + return true; + }); + } + }); + if (0 < schemaResponseError.length) { + throw new BadRequestException(schemaResponseError); + + } + + } + const agentDetails = await this.issuanceRepository.getAgentEndPoint(orgId); if (!agentDetails) { @@ -70,25 +101,22 @@ export class IssuanceService { const issuanceMethodLabel = 'create-offer'; const url = await this.getAgentUrl(issuanceMethodLabel, orgAgentType, agentEndPoint, agentDetails?.tenantId); - let apiKey; - apiKey = await this.cacheService.get(CommonConstants.CACHE_APIKEY_KEY); - if (!apiKey || null === apiKey || undefined === apiKey) { - apiKey = await this._getOrgAgentApiKey(orgId); - } const issueData: IIssueData = { protocolVersion: 'v1', connectionId, credentialFormats: { indy: { - attributes, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + attributes: (attributes).map(({ isRequired, ...rest }) => rest), credentialDefinitionId + } }, autoAcceptCredential: payload.autoAcceptCredential || 'always', comment }; - const credentialCreateOfferDetails: ICreateOfferResponse = await this._sendCredentialCreateOffer(issueData, url, apiKey); + const credentialCreateOfferDetails: ICreateOfferResponse = await this._sendCredentialCreateOffer(issueData, url, orgId); if (credentialCreateOfferDetails && 0 < Object.keys(credentialCreateOfferDetails).length) { delete credentialCreateOfferDetails._tags; @@ -114,15 +142,50 @@ export class IssuanceService { } } - - async sendCredentialOutOfBand(payload: OOBIssueCredentialDto): Promise<{ response: object; }> { + async sendCredentialOutOfBand(payload: OOBIssueCredentialDto): Promise<{ response: object }> { try { - const { orgId, credentialDefinitionId, comment, attributes, protocolVersion } = payload; + + const { orgId, credentialDefinitionId, comment, attributes, protocolVersion, credential, options, credentialType, isShortenUrl, reuseConnection } = payload; + if (credentialType === IssueCredentialType.INDY) { + const schemadetailsResponse: SchemaDetails = await this.issuanceRepository.getCredentialDefinitionDetails( + credentialDefinitionId + ); + + if (schemadetailsResponse?.attributes) { + const schemadetailsResponseError = []; + const attributesArray: IAttributes[] = JSON.parse(schemadetailsResponse.attributes); + + attributesArray.forEach((attribute) => { + if (attribute.attributeName && attribute.isRequired) { + + payload.attributes.map((attr) => { + if (attr.name === attribute.attributeName && attribute.isRequired && !attr.value) { + schemadetailsResponseError.push( + `Attribute '${attribute.attributeName}' is required but has an empty value.` + ); + } + return true; + }); + } + }); + if (0 < schemadetailsResponseError.length) { + throw new BadRequestException(schemadetailsResponseError); + } + + } + } + const agentDetails = await this.issuanceRepository.getAgentEndPoint(orgId); - // eslint-disable-next-line camelcase - // const platformConfig: platform_config = await this.issuanceRepository.getPlatformConfigDetails(); + let recipientKey: string | undefined; + if (true === reuseConnection) { + const data: agent_invitations[] = await this.issuanceRepository.getRecipientKeyByOrgId(orgId); + if (data && 0 < data.length) { + const [firstElement] = data; + recipientKey = firstElement?.recipientKey ?? undefined; + } + } + const { agentEndPoint, organisation } = agentDetails; - const { agentEndPoint } = agentDetails; if (!agentDetails) { throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); } @@ -132,32 +195,59 @@ export class IssuanceService { const issuanceMethodLabel = 'create-offer-oob'; const url = await this.getAgentUrl(issuanceMethodLabel, orgAgentType, agentEndPoint, agentDetails?.tenantId); - let apiKey; - apiKey = await this.cacheService.get(CommonConstants.CACHE_APIKEY_KEY); - if (!apiKey || null === apiKey || undefined === apiKey) { - apiKey = await this._getOrgAgentApiKey(orgId); - } - const issueData = { - protocolVersion: protocolVersion || 'v1', - credentialFormats: { - indy: { - attributes, - credentialDefinitionId - } - }, - autoAcceptCredential: payload.autoAcceptCredential || 'always', - goalCode: payload.goalCode || undefined, - parentThreadId: payload.parentThreadId || undefined, - willConfirm: payload.willConfirm || undefined, - label: payload.label || undefined, - comment: comment || '' - }; - const credentialCreateOfferDetails = await this._outOfBandCredentialOffer(issueData, url, apiKey); + let issueData; + if (credentialType === IssueCredentialType.INDY) { + issueData = { + protocolVersion: protocolVersion || 'v1', + credentialFormats: { + indy: { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + attributes: (attributes).map(({ isRequired, ...rest }) => rest), + credentialDefinitionId + } + }, + autoAcceptCredential: payload.autoAcceptCredential || 'always', + goalCode: payload.goalCode || undefined, + parentThreadId: payload.parentThreadId || undefined, + willConfirm: payload.willConfirm || undefined, + imageUrl: organisation?.logoUrl || payload?.imageUrl || undefined, + label: organisation?.name, + comment: comment || '', + recipientKey:recipientKey || undefined + }; + + } + + if (credentialType === IssueCredentialType.JSONLD) { + issueData = { + protocolVersion: protocolVersion || 'v2', + credentialFormats: { + jsonld: { + credential, + options + } + }, + autoAcceptCredential: payload.autoAcceptCredential || 'always', + goalCode: payload.goalCode || undefined, + parentThreadId: payload.parentThreadId || undefined, + willConfirm: payload.willConfirm || undefined, + imageUrl: organisation?.logoUrl || payload?.imageUrl || undefined, + label: organisation?.name, + comment: comment || '', + recipientKey:recipientKey || undefined + }; + } + const credentialCreateOfferDetails = await this._outOfBandCredentialOffer(issueData, url, orgId); + if (isShortenUrl) { + const invitationUrl: string = credentialCreateOfferDetails.response?.invitationUrl; + const url: string = await this.storeIssuanceObjectReturnUrl(invitationUrl); + credentialCreateOfferDetails.response['invitationUrl'] = url; + } return credentialCreateOfferDetails; } catch (error) { - this.logger.error(`[sendCredentialCreateOffer] - error in create credentials : ${JSON.stringify(error)}`); + this.logger.error(`[storeIssuanceObjectReturnUrl] - error in create credentials : ${JSON.stringify(error)}`); const errorStack = error?.status?.message?.error; if (errorStack) { @@ -172,6 +262,21 @@ export class IssuanceService { } } + async storeIssuanceObjectReturnUrl(storeObj: string): Promise { + try { + // Set default to false, since currently our invitation are not multi-use + const persistent: boolean = false; + //nats call in agent-service to create an invitation url + const pattern = { cmd: 'store-object-return-url' }; + const payload = { persistent, storeObj }; + const message = await this.natsCall(pattern, payload); + return message.response; + } catch (error) { + this.logger.error(`[storeIssuanceObjectReturnUrl] [NATS call]- error in storing object and returning url : ${JSON.stringify(error)}`); + throw error; + } + } + // Created this function to avoid the impact of actual "natsCall" function for other operations // Once implement this for all component then we'll remove the duplicate function async natsCallAgent(pattern: IPattern, payload: ISendOfferNatsPayload): Promise { @@ -220,10 +325,10 @@ export class IssuanceService { } } - async _sendCredentialCreateOffer(issueData: IIssueData, url: string, apiKey: string): Promise { + async _sendCredentialCreateOffer(issueData: IIssueData, url: string, orgId: string): Promise { try { const pattern = { cmd: 'agent-send-credential-create-offer' }; - const payload: ISendOfferNatsPayload = { issueData, url, apiKey }; + const payload: ISendOfferNatsPayload = { issueData, url, orgId }; return await this.natsCallAgent(pattern, payload); } catch (error) { this.logger.error(`[_sendCredentialCreateOffer] [NATS call]- error in create credentials : ${JSON.stringify(error)}`); @@ -292,13 +397,8 @@ export class IssuanceService { const issuanceMethodLabel = 'get-issue-credential-by-credential-id'; const url = await this.getAgentUrl(issuanceMethodLabel, orgAgentType, agentEndPoint, agentDetails?.tenantId, credentialRecordId); - // const apiKey = platformConfig?.sgApiKey; - // const apiKey = await this._getOrgAgentApiKey(orgId); - let apiKey: string = await this.cacheService.get(CommonConstants.CACHE_APIKEY_KEY); - if (!apiKey || null === apiKey || undefined === apiKey) { - apiKey = await this._getOrgAgentApiKey(orgId); - } - const createConnectionInvitation = await this._getIssueCredentialsbyCredentialRecordId(url, apiKey); + + const createConnectionInvitation = await this._getIssueCredentialsbyCredentialRecordId(url, orgId); return createConnectionInvitation?.response; } catch (error) { this.logger.error(`[getIssueCredentialsbyCredentialRecordId] - error in get credentials : ${JSON.stringify(error)}`); @@ -324,12 +424,12 @@ export class IssuanceService { } } - async _getIssueCredentialsbyCredentialRecordId(url: string, apiKey: string): Promise<{ + async _getIssueCredentialsbyCredentialRecordId(url: string, orgId: string): Promise<{ response: string; }> { try { const pattern = { cmd: 'agent-get-issued-credentials-by-credentialDefinitionId' }; - const payload = { url, apiKey }; + const payload = { url, orgId }; return await this.natsCall(pattern, payload); } catch (error) { @@ -338,180 +438,293 @@ export class IssuanceService { } } +async outOfBandCredentialOffer(outOfBandCredential: OutOfBandCredentialOfferPayload): Promise { + try { + const { + credentialOffer, + comment, + credentialDefinitionId, + orgId, + protocolVersion, + attributes, + emailId, + credentialType + } = outOfBandCredential; + + if (IssueCredentialType.INDY === credentialType) { + const schemaResponse: SchemaDetails = await this.issuanceRepository.getCredentialDefinitionDetails( + credentialDefinitionId + ); - async outOfBandCredentialOffer(outOfBandCredential: OutOfBandCredentialOfferPayload): Promise { - try { - const { - credentialOffer, - comment, - credentialDefinitionId, - orgId, - protocolVersion - } = outOfBandCredential; - - const agentDetails = await this.issuanceRepository.getAgentEndPoint(orgId); - if (!agentDetails) { - throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); + let attributesArray: IAttributes[] = []; + if (schemaResponse?.attributes) { + attributesArray = JSON.parse(schemaResponse.attributes); } - const orgAgentType = await this.issuanceRepository.getOrgAgentType(agentDetails?.orgAgentTypeId); - - const issuanceMethodLabel = 'create-offer-oob'; - const url = await this.getAgentUrl(issuanceMethodLabel, orgAgentType, agentDetails.agentEndPoint, agentDetails.tenantId); - const organizationDetails = await this.issuanceRepository.getOrganization(orgId); - - if (!organizationDetails) { - throw new NotFoundException(ResponseMessages.issuance.error.organizationNotFound); + if (0 < attributes?.length) { + const attrError = []; + attributesArray.forEach((schemaAttribute, i) => { + if (schemaAttribute.isRequired) { + const attribute = attributes.find((attribute) => attribute.name === schemaAttribute.attributeName); + if (!attribute?.value) { + attrError.push(`attributes.${i}.Attribute ${schemaAttribute.attributeName} is required`); + } + } + }); + if (0 < attrError.length) { + throw new BadRequestException(attrError); + } } - - // if (!(credentialOffer && 0 < credentialOffer.length)) { - // throw new NotFoundException(ResponseMessages.issuance.error.credentialOfferNotFound); - // } - - let apiKey: string = await this.cacheService.get(CommonConstants.CACHE_APIKEY_KEY); - if (!apiKey || null === apiKey || undefined === apiKey) { - apiKey = await this._getOrgAgentApiKey(orgId); + if (0 < credentialOffer?.length) { + const credefError = []; + credentialOffer.forEach((credentialAttribute, index) => { + attributesArray.forEach((schemaAttribute, i) => { + const attribute = credentialAttribute.attributes.find( + (attribute) => attribute.name === schemaAttribute.attributeName + ); + + if (schemaAttribute.isRequired && !attribute?.value) { + credefError.push( + `credentialOffer.${index}.attributes.${i}.Attribute ${schemaAttribute.attributeName} is required` + ); + } + }); + }); + if (0 < credefError.length) { + throw new BadRequestException(credefError); + } } + } - const errors = []; - const emailPromises = []; - - const sendEmailForCredentialOffer = async (iterator, emailId, index): Promise => { - const iterationNo = index + 1; - try { - const outOfBandIssuancePayload = { - protocolVersion: protocolVersion || 'v1', - credentialFormats: { - indy: { - attributes: iterator.attributes, - credentialDefinitionId - } - }, - autoAcceptCredential: outOfBandCredential.autoAcceptCredential || 'always', - comment, - goalCode: outOfBandCredential.goalCode || undefined, - parentThreadId: outOfBandCredential.parentThreadId || undefined, - willConfirm: outOfBandCredential.willConfirm || undefined, - label: outOfBandCredential.label || undefined - }; - - const credentialCreateOfferDetails = await this._outOfBandCredentialOffer(outOfBandIssuancePayload, url, apiKey); - - if (!credentialCreateOfferDetails) { - errors.push(new NotFoundException(ResponseMessages.issuance.error.credentialOfferNotFound)); - return false; - } + const agentDetails = await this.issuanceRepository.getAgentEndPoint(orgId); - const invitationId = credentialCreateOfferDetails.response.invitation['@id']; + const { organisation } = agentDetails; + if (!agentDetails) { + throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); + } - if (!invitationId) { - errors.push(new NotFoundException(ResponseMessages.issuance.error.invitationNotFound)); - return false; - } + const orgAgentType = await this.issuanceRepository.getOrgAgentType(agentDetails?.orgAgentTypeId); - const agentEndPoint = agentDetails.tenantId - ? `${agentDetails.agentEndPoint}/multi-tenancy/url/${agentDetails.tenantId}/${invitationId}` - : `${agentDetails.agentEndPoint}/url/${invitationId}`; + const issuanceMethodLabel = 'create-offer-oob'; + const url = await this.getAgentUrl( + issuanceMethodLabel, + orgAgentType, + agentDetails.agentEndPoint, + agentDetails.tenantId + ); + const organizationDetails = await this.issuanceRepository.getOrganization(orgId); - const qrCodeOptions = { type: 'image/png' }; - const outOfBandIssuanceQrCode = await QRCode.toDataURL(agentEndPoint, qrCodeOptions); - const platformConfigData = await this.issuanceRepository.getPlatformConfigDetails(); + if (!organizationDetails) { + throw new NotFoundException(ResponseMessages.issuance.error.organizationNotFound); + } - if (!platformConfigData) { - errors.push(new NotFoundException(ResponseMessages.issuance.error.platformConfigNotFound)); - return false; - } + const errors = []; + let credentialOfferResponse; + const arraycredentialOfferResponse = []; + const sendEmailCredentialOffer: { + iterator: CredentialOffer; + emailId: string; + index: number; + credentialType: IssueCredentialType; + protocolVersion: string; + attributes: IAttributes[]; + credentialDefinitionId: string; + outOfBandCredential: OutOfBandCredentialOfferPayload; + comment: string; + organisation: organisation; + errors: string[]; + url: string; + orgId: string; + organizationDetails: organisation; + } = { + credentialType, + protocolVersion, + attributes, + credentialDefinitionId, + outOfBandCredential, + comment, + organisation, + errors, + url, + orgId, + organizationDetails, + iterator: undefined, + emailId: emailId || '', + index: 0 + }; - this.emailData.emailFrom = platformConfigData.emailFrom; - this.emailData.emailTo = emailId; - this.emailData.emailSubject = `${process.env.PLATFORM_NAME} Platform: Issuance of Your Credential`; - this.emailData.emailHtml = this.outOfBandIssuance.outOfBandIssuance(emailId, organizationDetails.name, agentEndPoint); - this.emailData.emailAttachments = [ - { - filename: 'qrcode.png', - content: outOfBandIssuanceQrCode.split(';base64,')[1], - contentType: 'image/png', - disposition: 'attachment' - } - ]; + if (credentialOffer) { - const isEmailSent = await sendEmail(this.emailData); + for (const [index, iterator] of credentialOffer.entries()) { + sendEmailCredentialOffer['iterator'] = iterator; + sendEmailCredentialOffer['emailId'] = iterator.emailId; + sendEmailCredentialOffer['index'] = index; + + await this.delay(500); // Wait for 0.5 seconds + const sendOobOffer = await this.sendEmailForCredentialOffer(sendEmailCredentialOffer); - if (!isEmailSent) { - errors.push(new InternalServerErrorException(ResponseMessages.issuance.error.emailSend)); - return false; - } + arraycredentialOfferResponse.push(sendOobOffer); + } + if (0 < errors.length) { + throw errors; + } + + return arraycredentialOfferResponse.every((result) => true === result); + } else { + credentialOfferResponse = await this.sendEmailForCredentialOffer(sendEmailCredentialOffer); + return credentialOfferResponse; + } + } catch (error) { + this.logger.error( + `[outOfBoundCredentialOffer] - error in create out-of-band credentials: ${JSON.stringify(error)}` + ); + if (0 < error?.length) { + const errorStack = error?.map((item) => { + const { message, statusCode, error } = item?.error || item?.response || {}; + return { + message, + statusCode, + error + }; + }); + throw new RpcException({ + error: errorStack, + statusCode: error?.status?.code, + message: ResponseMessages.issuance.error.unableToCreateOOBOffer + }); + } else { + throw new RpcException(error.response ? error.response : error); + } + } +} - return isEmailSent; - } catch (error) { - this.logger.error('[OUT-OF-BAND CREATE OFFER - SEND EMAIL]::', JSON.stringify(error)); - const errorStack = error?.status?.message; - if (errorStack) { - errors.push( - new RpcException({ - error: `${errorStack?.error?.message} at position ${iterationNo}`, - statusCode: errorStack?.statusCode, - message: `${ResponseMessages.issuance.error.walletError} at position ${iterationNo}` - })); - } else { - errors.push(new InternalServerErrorException(`${error.message} at position ${iterationNo}`)); +async sendEmailForCredentialOffer(sendEmailCredentialOffer: SendEmailCredentialOffer): Promise { + const { + iterator, + emailId, + index, + credentialType, + protocolVersion, + attributes, + credentialDefinitionId, + outOfBandCredential, + comment, + organisation, + errors, + url, + orgId, + organizationDetails + } = sendEmailCredentialOffer; + + const iterationNo = index + 1; + try { + let outOfBandIssuancePayload; + if (IssueCredentialType.INDY === credentialType) { + + outOfBandIssuancePayload = { + protocolVersion: protocolVersion || 'v1', + credentialFormats: { + indy: { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + attributes: attributes ? attributes : iterator.attributes.map(({ isRequired, ...rest }) => rest), + credentialDefinitionId } - return false; - } + }, + autoAcceptCredential: outOfBandCredential.autoAcceptCredential || 'always', + comment, + goalCode: outOfBandCredential.goalCode || undefined, + parentThreadId: outOfBandCredential.parentThreadId || undefined, + willConfirm: outOfBandCredential.willConfirm || undefined, + label: organisation?.name, + imageUrl: organisation?.logoUrl || outOfBandCredential?.imageUrl }; + } - if (credentialOffer) { - for (let i = 0; i < credentialOffer.length; i += Number(process.env.OOB_BATCH_SIZE)) { - const batch = credentialOffer.slice(i, i + Number(process.env.OOB_BATCH_SIZE)); - - // Process each batch in parallel - const batchPromises = batch.map((iterator, index) => sendEmailForCredentialOffer(iterator, iterator.emailId, index)); - emailPromises.push(Promise.all(batchPromises)); - } - } + if (IssueCredentialType.JSONLD === credentialType) { + outOfBandIssuancePayload = { + protocolVersion: 'v2', + credentialFormats: { + jsonld: { + credential: iterator.credential, + options: iterator.options + } + }, + autoAcceptCredential: outOfBandCredential.autoAcceptCredential || 'always', + comment, + goalCode: outOfBandCredential.goalCode || undefined, + parentThreadId: outOfBandCredential.parentThreadId || undefined, + willConfirm: outOfBandCredential.willConfirm || undefined, + label: organisation?.name, + imageUrl: organisation?.logoUrl || outOfBandCredential?.imageUrl + }; + } - const results = await Promise.all(emailPromises); + const credentialCreateOfferDetails = await this._outOfBandCredentialOffer(outOfBandIssuancePayload, url, orgId); - // Flatten the results array - const flattenedResults = [].concat(...results); + if (!credentialCreateOfferDetails) { + errors.push(new NotFoundException(ResponseMessages.issuance.error.credentialOfferNotFound)); + return false; + } - // Check if all emails were successfully sent - const allSuccessful = flattenedResults.every((result) => true === result); + const invitationUrl: string = credentialCreateOfferDetails.response?.invitationUrl; + const shortenUrl: string = await this.storeIssuanceObjectReturnUrl(invitationUrl); - if (0 < errors.length) { - throw errors; - } + if (!invitationUrl) { + errors.push(new NotFoundException(ResponseMessages.issuance.error.invitationNotFound)); + return false; + } + const qrCodeOptions = { type: 'image/png' }; + const outOfBandIssuanceQrCode = await QRCode.toDataURL(shortenUrl, qrCodeOptions); + const platformConfigData = await this.issuanceRepository.getPlatformConfigDetails(); + if (!platformConfigData) { + errors.push(new NotFoundException(ResponseMessages.issuance.error.platformConfigNotFound)); + return false; + } + this.emailData.emailFrom = platformConfigData.emailFrom; + this.emailData.emailTo = emailId; + this.emailData.emailSubject = `${process.env.PLATFORM_NAME} Platform: Issuance of Your Credential`; + this.emailData.emailHtml = this.outOfBandIssuance.outOfBandIssuance(emailId, organizationDetails.name, shortenUrl); + this.emailData.emailAttachments = [ + { + filename: 'qrcode.png', + content: outOfBandIssuanceQrCode.split(';base64,')[1], + contentType: 'image/png', + disposition: 'attachment' + } + ]; + const isEmailSent = await sendEmail(this.emailData); + this.logger.log(`isEmailSent ::: ${JSON.stringify(isEmailSent)}`); + if (!isEmailSent) { + errors.push(new InternalServerErrorException(ResponseMessages.issuance.error.emailSend)); + return false; + } - return allSuccessful; - } catch (error) { - this.logger.error(`[outOfBoundCredentialOffer] - error in create out-of-band credentials: ${JSON.stringify(error)}`); - if (0 < error?.length) { - const errorStack = error?.map(item => { - const { message, statusCode, error } = item?.error || item?.response || {}; - return { - message, - statusCode, - error - }; - }); - throw new RpcException({ - error: errorStack, - statusCode: error?.status?.code, - message: ResponseMessages.issuance.error.unableToCreateOOBOffer - }); - } else { - throw new RpcException(error.response ? error.response : error); - } + return isEmailSent; + + } catch (error) { + this.logger.error('[OUT-OF-BAND CREATE OFFER - SEND EMAIL]::', JSON.stringify(error)); + const errorStack = error?.status?.message; + if (errorStack) { + errors.push( + new RpcException({ + error: `${errorStack?.error?.message} at position ${iterationNo}`, + statusCode: errorStack?.statusCode, + message: `${ResponseMessages.issuance.error.walletError} at position ${iterationNo}` + }) + ); + } else { + errors.push(new InternalServerErrorException(`${error.message} at position ${iterationNo}`)); } + return false; } +} - - async _outOfBandCredentialOffer(outOfBandIssuancePayload: object, url: string, apiKey: string): Promise<{ + async _outOfBandCredentialOffer(outOfBandIssuancePayload: object, url: string, orgId: string): Promise<{ response; }> { try { const pattern = { cmd: 'agent-out-of-band-credential-offer' }; - const payload = { outOfBandIssuancePayload, url, apiKey }; + const payload = { outOfBandIssuancePayload, url, orgId }; return await this.natsCall(pattern, payload); } catch (error) { this.logger.error(`[_outOfBandCredentialOffer] [NATS call]- error in out of band : ${JSON.stringify(error)}`); @@ -520,10 +733,10 @@ export class IssuanceService { } /** - * Description: Fetch agent url - * @param referenceId - * @returns agent URL - */ + * Description: Fetch agent url + * @param referenceId + * @returns agent URL + */ async getAgentUrl( issuanceMethodLabel: string, orgAgentType: string, @@ -632,7 +845,7 @@ export class IssuanceService { } - async importAndPreviewDataForIssuance(importFileDetails: ImportFileDetails): Promise { + async importAndPreviewDataForIssuance(importFileDetails: ImportFileDetails, requestId?: string): Promise { try { const credDefResponse = @@ -668,12 +881,10 @@ export class IssuanceService { // Output invalid emails if (0 < invalidEmails.length) { - throw new BadRequestException(`Invalid emails found in the chosen file`); - } - const fileData: string[] = parsedData.data.map(Object.values); + const fileData: string[][] = parsedData.data.map(Object.values); const fileHeader: string[] = parsedData.meta.fields; const attributesArray = JSON.parse(credDefResponse.attributes); @@ -688,7 +899,7 @@ export class IssuanceService { } await this.validateFileHeaders(fileHeader, attributeNameArray); - await this.validateFileData(fileData); + await this.validateFileData(fileData, attributesArray, fileHeader); const resData = { schemaLedgerId: credDefResponse.schemaLedgerId, @@ -699,16 +910,13 @@ export class IssuanceService { const newCacheKey = uuidv4(); - await this.cacheManager.set(newCacheKey, JSON.stringify(resData), 3600); + await this.cacheManager.set(requestId ? requestId : newCacheKey, JSON.stringify(resData), 60000); return newCacheKey; } catch (error) { - this.logger.error(`error in validating credentials : ${error}`); + this.logger.error(`error in validating credentials : ${error.response}`); throw new RpcException(error.response ? error.response : error); - } finally { - // await this.awsService.deleteFile(importFileDetails.fileKey); - // this.logger.error(`Deleted uploaded file after processing.`); } } @@ -802,7 +1010,7 @@ export class IssuanceService { return new Promise(resolve => setTimeout(resolve, ms)); } - async issueBulkCredential(requestId: string, orgId: string, clientDetails: ClientDetails): Promise { + async issueBulkCredential(requestId: string, orgId: string, clientDetails: IClientDetails, reqPayload: ImportFileDetails): Promise { const fileUpload: { lastChangedDateTime: Date; name?: string; @@ -825,11 +1033,17 @@ export class IssuanceService { } try { - const cachedData = await this.cacheManager.get(requestId); + let cachedData = await this.cacheManager.get(requestId); if (!cachedData) { throw new BadRequestException(ResponseMessages.issuance.error.cacheTimeOut); } + if (cachedData && clientDetails?.isSelectiveIssuance) { + await this.cacheManager.del(requestId); + await this.importAndPreviewDataForIssuance(reqPayload, requestId); + // await this.cacheManager.set(requestId, reqPayload); + cachedData = await this.cacheManager.get(requestId); + } const parsedData = JSON.parse(cachedData as string).fileData.data; const parsedPrimeDetails = JSON.parse(cachedData as string); @@ -974,15 +1188,22 @@ export class IssuanceService { fileUploadData.createDateTime = new Date(); fileUploadData.referenceId = jobDetails.data.email; fileUploadData.jobId = jobDetails.id; + const { orgId } = jobDetails; + + const agentDetails = await this.issuanceRepository.getAgentEndPoint(orgId); + // eslint-disable-next-line camelcase + const { organisation } = agentDetails; let isErrorOccurred = false; try { const oobIssuancepayload = { credentialDefinitionId: jobDetails.credentialDefinitionId, orgId: jobDetails.orgId, + label: organisation?.name, attributes: [], - emailId: jobDetails.data.email + emailId: jobDetails.data.email, + credentialType: IssueCredentialType.INDY }; for (const key in jobDetails.data) { @@ -1021,8 +1242,8 @@ export class IssuanceService { 0 === errorCount ? FileUploadStatus.completed : FileUploadStatus.partially_completed; if (!jobDetails.isRetry) { + socket.emit('bulk-issuance-process-completed', { clientId: jobDetails.clientId, fileUploadId: jobDetails.fileUploadId }); this.cacheManager.del(jobDetails.cacheId); - socket.emit('bulk-issuance-process-completed', {clientId: jobDetails.clientId, fileUploadId: jobDetails.fileUploadId}); } else { socket.emit('bulk-issuance-process-retry-completed', { clientId: jobDetails.clientId }); } @@ -1031,7 +1252,6 @@ export class IssuanceService { status, lastChangedDateTime: new Date() }); - } } catch (error) { this.logger.error(`Error in completing bulk issuance process: ${error}`); @@ -1051,23 +1271,22 @@ export class IssuanceService { ): Promise { try { const fileSchemaHeader: string[] = fileHeader.slice(); - - if ('email' === fileHeader[0]) { - fileSchemaHeader.splice(0, 1); + if ('email' === fileHeader[0]) { + fileSchemaHeader.splice(0, 1); } else { throw new BadRequestException(ResponseMessages.bulkIssuance.error.emailColumn ); } if (schemaAttributes.length !== fileSchemaHeader.length) { - throw new BadRequestException(ResponseMessages.bulkIssuance.error.attributeNumber + throw new ConflictException(ResponseMessages.bulkIssuance.error.attributeNumber ); } const mismatchedAttributes = fileSchemaHeader.filter(value => !schemaAttributes.includes(value)); if (0 < mismatchedAttributes.length) { - throw new BadRequestException(ResponseMessages.bulkIssuance.error.mismatchedAttributes); + throw new ConflictException(ResponseMessages.bulkIssuance.error.mismatchedAttributes); } } catch (error) { throw error; @@ -1075,25 +1294,38 @@ export class IssuanceService { } } - async validateFileData(fileData: string[]): Promise { - let rowIndex: number = 0; - let columnIndex: number = 0; - const isNullish = Object.values(fileData).some((value) => { - columnIndex = 0; - rowIndex++; - const isFalsyForColumnValue = Object.values(value).some((colvalue) => { - columnIndex++; - if (null === colvalue || '' == colvalue) { - return true; - } - return false; + async validateFileData(fileData: string[][], attributesArray: { attributeName: string, schemaDataType: string, displayName: string, isRequired: boolean }[], fileHeader: string[]): Promise { + try { + const filedata = fileData.map((item: string[]) => { + const fileHeaderData = item?.map((element, j) => ({ + value: element, + header: fileHeader[j] + })); + return fileHeaderData; }); - return isFalsyForColumnValue; - }); - if (isNullish) { - throw new BadRequestException( - `Empty data found at row ${rowIndex} and column ${columnIndex}` - ); + + const errorFileData = []; + + filedata.forEach((attr, i) => { + attr.forEach((eachElement) => { + + attributesArray.forEach((eachItem) => { + if (eachItem.attributeName === eachElement.header) { + if (eachItem.isRequired && !eachElement.value) { + errorFileData.push(`Attribute ${eachItem.attributeName} is required at row ${i + 1}`); + } + } + }); + return eachElement; + }); + return attr; + }); + + if (0 < errorFileData.length) { + throw new BadRequestException(errorFileData); + } + } catch (error) { + throw error; } } diff --git a/apps/issuance/templates/out-of-band-issuance.template.ts b/apps/issuance/templates/out-of-band-issuance.template.ts index 9ac9b4ade..a2f68176d 100644 --- a/apps/issuance/templates/out-of-band-issuance.template.ts +++ b/apps/issuance/templates/out-of-band-issuance.template.ts @@ -13,7 +13,7 @@ export class OutOfBandIssuance {
- CREDEBL logo + ${process.env.PLATFORM_NAME} logo
@@ -25,14 +25,14 @@ export class OutOfBandIssuance { To acknowledge and access your credential, kindly proceed with following steps:
    -
  • Download the ADEYA SSI App from - Android Play Store or -iOS App Store. (Skip, if already downloaded) +
  • Download the ${process.env.MOBILE_APP_NAME} from + Android Play Store or +iOS App Store. (Skip, if already downloaded)
  • -
  • Complete the onboarding process in ADEYA.
  • -
  • Open the “Accept Credential” link below in this email (This will open the link in the ADEYA App)
  • -
  • Accept the Credential in ADEYA.
  • -
  • Check "Credentials" tab in ADEYA to view the issued credential.
  • +
  • Complete the onboarding process in ${process.env.MOBILE_APP}.
  • +
  • Open the “Accept Credential” link below in this email (This will open the link in the ${process.env.MOBILE_APP} App)
  • +
  • Accept the Credential in ${process.env.MOBILE_APP}.
  • +
  • Check "Credentials" tab in ${process.env.MOBILE_APP} to view the issued credential.

- Note: If the above steps do not work for you, please open the attached QR Code image in this email on another device, and scan the QR code using the ADEYA SSI App on your mobile device. + Note: If the above steps do not work for you, please open the attached QR Code image in this email on another device, and scan the QR code using the ${process.env.MOBILE_APP_NAME} on your mobile device. The QR Code is single-use.

@@ -52,11 +52,11 @@ export class OutOfBandIssuance {
- For any assistance or questions while accessing your account, please do not hesitate to contact the support team at support@blockster.global. Our team will ensure a seamless onboarding experience for you. + For any assistance or questions while accessing your account, please do not hesitate to contact the support team at ${process.env.PUBLIC_PLATFORM_SUPPORT_EMAIL}. Our team will ensure a seamless onboarding experience for you.

- © Blockster Labs Pvt. Ltd. + © ${process.env.POWERED_BY}

diff --git a/apps/ledger/src/credential-definition/credential-definition.service.ts b/apps/ledger/src/credential-definition/credential-definition.service.ts index c68bfed2d..1d19e5041 100644 --- a/apps/ledger/src/credential-definition/credential-definition.service.ts +++ b/apps/ledger/src/credential-definition/credential-definition.service.ts @@ -17,7 +17,6 @@ import { map } from 'rxjs/operators'; import { OrgAgentType } from '@credebl/enum/enum'; import { Cache } from 'cache-manager'; import { CACHE_MANAGER } from '@nestjs/cache-manager'; -import { CommonConstants } from '@credebl/common/common.constant'; @Injectable() export class CredentialDefinitionService extends BaseService { constructor( @@ -36,13 +35,8 @@ export class CredentialDefinitionService extends BaseService { // eslint-disable-next-line yoda const did = credDef.orgDid?.split(':').length >= 4 ? credDef.orgDid : orgDid; const getAgentDetails = await this.credentialDefinitionRepository.getAgentType(credDef.orgId); - // const apiKey = await this._getOrgAgentApiKey(credDef.orgId); - let apiKey:string = await this.cacheService.get(CommonConstants.CACHE_APIKEY_KEY); - this.logger.log(`cachedApiKey----${apiKey}`); - if (!apiKey || null === apiKey || undefined === apiKey) { - apiKey = await this._getOrgAgentApiKey(credDef.orgId); - } - const { userId } = user.selectedOrg; + + const userId = user.id; credDef.tag = credDef.tag.trim(); const dbResult: credential_definition = await this.credentialDefinitionRepository.getByAttribute( credDef.schemaLedgerId, @@ -61,7 +55,7 @@ export class CredentialDefinitionService extends BaseService { schemaId: credDef.schemaLedgerId, issuerId: did, agentEndPoint, - apiKey, + orgId: credDef.orgId, agentType: OrgAgentType.DEDICATED }; @@ -79,7 +73,7 @@ export class CredentialDefinitionService extends BaseService { issuerId: did }, agentEndPoint, - apiKey, + orgId: credDef.orgId, agentType: OrgAgentType.SHARED }; credDefResponseFromAgentService = await this._createCredentialDefinition(CredDefPayload); @@ -182,17 +176,12 @@ export class CredentialDefinitionService extends BaseService { const { agentEndPoint } = await this.credentialDefinitionRepository.getAgentDetailsByOrgId(String(orgId)); const getAgentDetails = await this.credentialDefinitionRepository.getAgentType(String(orgId)); const orgAgentType = await this.credentialDefinitionRepository.getOrgAgentType(getAgentDetails.org_agents[0].orgAgentTypeId); - // const apiKey = await this._getOrgAgentApiKey(String(orgId)); - let apiKey:string = await this.cacheService.get(CommonConstants.CACHE_APIKEY_KEY); - this.logger.log(`cachedApiKey----${apiKey}`); - if (!apiKey || null === apiKey || undefined === apiKey) { - apiKey = await this._getOrgAgentApiKey(String(orgId)); - } + let credDefResponse; if (OrgAgentType.DEDICATED === orgAgentType) { const getSchemaPayload = { credentialDefinitionId, - apiKey, + orgId, agentEndPoint, agentType: OrgAgentType.DEDICATED }; diff --git a/apps/ledger/src/credential-definition/interfaces/credential-definition.interface.ts b/apps/ledger/src/credential-definition/interfaces/credential-definition.interface.ts index b290bce9a..3fcd058e1 100644 --- a/apps/ledger/src/credential-definition/interfaces/credential-definition.interface.ts +++ b/apps/ledger/src/credential-definition/interfaces/credential-definition.interface.ts @@ -8,6 +8,7 @@ export interface CreateCredDefAgentRedirection { agentType?: string; apiKey?: string; agentEndPoint?: string; + orgId?: string; } export interface ITenantCredDef { @@ -24,6 +25,7 @@ export interface GetCredDefAgentRedirection { agentEndPoint?: string; agentType?: string; method?: string; + orgId?: string; } export interface GetCredDefFromTenantPayload { diff --git a/apps/ledger/src/credential-definition/interfaces/index.ts b/apps/ledger/src/credential-definition/interfaces/index.ts index b994e46a8..d4be794cd 100644 --- a/apps/ledger/src/credential-definition/interfaces/index.ts +++ b/apps/ledger/src/credential-definition/interfaces/index.ts @@ -1,6 +1,7 @@ import { UserRoleOrgPermsDto } from '../../schema/dtos/user-role-org-perms.dto'; export interface IUserRequestInterface { + id: string; userId: string; email: string; orgId: string; diff --git a/apps/ledger/src/schema/interfaces/schema-payload.interface.ts b/apps/ledger/src/schema/interfaces/schema-payload.interface.ts index 1b0c2797e..f56ffbfda 100644 --- a/apps/ledger/src/schema/interfaces/schema-payload.interface.ts +++ b/apps/ledger/src/schema/interfaces/schema-payload.interface.ts @@ -20,9 +20,11 @@ export interface ISchema { } export interface IAttributeValue { + isRequired: boolean; attributeName: string; schemaDataType: string; - displayName: string + displayName: string; + } export interface ISchemaPayload { @@ -65,3 +67,36 @@ export interface ISchemaCredDeffSearchInterface { user: IUserRequestInterface, } +export interface ISchemaExist { + schemaName: string; + version: string; +} + +export interface SchemaPayload { + schemaAttributes: W3CSchemaAttributes [], + schemaName: string, + did: string, + description: string + } + + export interface W3CSchemaAttributes { + type: string, + title: string, + } + +export interface W3CSchemaPayload { + schemaPayload: SchemaPayload, + orgId: string + } + +export interface W3CCreateSchema { + url: string, + orgId: string, + schemaRequestPayload: object +} + +export interface IdAttribute extends W3CSchemaAttributes { + format: string; + order?: string +} + diff --git a/apps/ledger/src/schema/interfaces/schema.interface.ts b/apps/ledger/src/schema/interfaces/schema.interface.ts index a67d45a2b..a89044961 100644 --- a/apps/ledger/src/schema/interfaces/schema.interface.ts +++ b/apps/ledger/src/schema/interfaces/schema.interface.ts @@ -1,6 +1,7 @@ import { UserRoleOrgPermsDto } from '../dtos/user-role-org-perms.dto'; export interface IUserRequestInterface { + id: string; userId: string; email: string; orgId: string; diff --git a/apps/ledger/src/schema/repositories/schema.repository.ts b/apps/ledger/src/schema/repositories/schema.repository.ts index 58c782f6a..bd84b2785 100644 --- a/apps/ledger/src/schema/repositories/schema.repository.ts +++ b/apps/ledger/src/schema/repositories/schema.repository.ts @@ -2,7 +2,7 @@ import { ConflictException, Injectable, InternalServerErrorException, Logger } from '@nestjs/common'; import { PrismaService } from '@credebl/prisma-service'; import { ledgers, org_agents, org_agents_type, organisation, schema } from '@prisma/client'; -import { ISchema, ISchemaSearchCriteria } from '../interfaces/schema-payload.interface'; +import { ISchema, ISchemaExist, ISchemaSearchCriteria } from '../interfaces/schema-payload.interface'; import { ResponseMessages } from '@credebl/common/response-messages'; import { AgentDetails, ISchemasWithCount } from '../interfaces/schema.interface'; import { SortValue } from '@credebl/enum/enum'; @@ -12,23 +12,18 @@ import { ICredDefWithCount } from '@credebl/common/interfaces/schema.interface'; export class SchemaRepository { private readonly logger = new Logger('SchemaRepository'); - constructor( - private prisma: PrismaService - ) { } + constructor(private prisma: PrismaService) {} async saveSchema(schemaResult: ISchema): Promise { try { if (schemaResult.schema.schemaName) { - const schema = await this.schemaExists( - schemaResult.schema.schemaName, - schemaResult.schema.schemaVersion - ); + const schema = await this.schemaExists(schemaResult.schema.schemaName, schemaResult.schema.schemaVersion); const schemaLength = 0; - if (schema.length !== schemaLength) { - throw new ConflictException( - ResponseMessages.schema.error.exists, - { cause: new Error(), description: ResponseMessages.errorMessages.conflict } - ); + if (schema.length !== schemaLength) { + throw new ConflictException(ResponseMessages.schema.error.exists, { + cause: new Error(), + description: ResponseMessages.errorMessages.conflict + }); } const saveResult = await this.prisma.schema.create({ data: { @@ -96,7 +91,7 @@ export class SchemaRepository { issuerId: true }, orderBy: { - [payload.sortField]: SortValue.ASC === payload.sortBy ? 'asc' : 'desc' + [payload.sortField]: SortValue.ASC === payload.sortBy ? 'asc' : 'desc' }, take: Number(payload.pageSize), skip: (payload.pageNumber - 1) * payload.pageSize @@ -111,11 +106,10 @@ export class SchemaRepository { return { schemasCount, schemasResult }; } catch (error) { this.logger.error(`Error in getting schemas: ${error}`); - throw new InternalServerErrorException( - ResponseMessages.schema.error.failedFetchSchema, - { cause: new Error(), description: error.message } - ); - + throw new InternalServerErrorException(ResponseMessages.schema.error.failedFetchSchema, { + cause: new Error(), + description: error.message + }); } } @@ -138,11 +132,13 @@ export class SchemaRepository { } } - async getAgentType(orgId: string): Promise { + async getAgentType(orgId: string): Promise< + organisation & { + org_agents: (org_agents & { + org_agent_type: org_agents_type; + })[]; + } + > { try { const agentDetails = await this.prisma.organisation.findUnique({ where: { @@ -164,16 +160,12 @@ export class SchemaRepository { } async getSchemasCredDeffList(payload: ISchemaSearchCriteria): Promise { + const { orgId, schemaId } = payload; - const {orgId, schemaId} = payload; - try { const credDefResult = await this.prisma.credential_definition.findMany({ where: { - AND: [ - { orgId }, - { schemaLedgerId: schemaId } - ] + AND: [{ orgId }, { schemaLedgerId: schemaId }] }, select: { tag: true, @@ -190,10 +182,7 @@ export class SchemaRepository { }); const credDefCount = await this.prisma.credential_definition.count({ where: { - AND: [ - { orgId }, - { schemaLedgerId: schemaId } - ] + AND: [{ orgId }, { schemaLedgerId: schemaId }] } }); return { credDefResult, credDefCount }; @@ -202,7 +191,7 @@ export class SchemaRepository { throw error; } } - + async getAllSchemaDetails(payload: ISchemaSearchCriteria): Promise<{ schemasCount: number; schemasResult: { @@ -216,7 +205,7 @@ export class SchemaRepository { issuerId: string; orgId: string; }[]; - }> { + }> { try { const schemasResult = await this.prisma.schema.findMany({ where: { @@ -240,7 +229,7 @@ export class SchemaRepository { issuerId: true }, orderBy: { - [payload.sortField]: 'desc' === payload.sortBy ? 'desc' : 'asc' + [payload.sortField]: 'desc' === payload.sortBy ? 'desc' : 'asc' }, take: Number(payload.pageSize), skip: (payload.pageNumber - 1) * payload.pageSize @@ -265,7 +254,6 @@ export class SchemaRepository { schemaLedgerId: schemaId } }); - } catch (error) { this.logger.error(`Error in getting get schema by schema ledger id: ${error}`); throw error; @@ -274,7 +262,6 @@ export class SchemaRepository { async getOrgAgentType(orgAgentId: string): Promise { try { - const { agent } = await this.prisma.org_agents_type.findFirst({ where: { id: orgAgentId @@ -295,10 +282,37 @@ export class SchemaRepository { indyNamespace: LedgerName } }); - } catch (error) { this.logger.error(`Error in getting get schema by schema ledger id: ${error}`); throw error; } } -} \ No newline at end of file + + async schemaExist(payload: ISchemaExist): Promise<{ + id: string; + createDateTime: Date; + createdBy: string; + lastChangedDateTime: Date; + lastChangedBy: string; + name: string; + version: string; + attributes: string; + schemaLedgerId: string; + publisherDid: string; + issuerId: string; + orgId: string; + ledgerId: string; + }[]> { + try { + return this.prisma.schema.findMany({ + where: { + name: payload.schemaName, + version: payload.version + } + }); + } catch (error) { + this.logger.error(`Error in getting get schema by name and version: ${error}`); + throw error; + } + } +} diff --git a/apps/ledger/src/schema/schema.controller.ts b/apps/ledger/src/schema/schema.controller.ts index 15df46d6a..e428607a0 100644 --- a/apps/ledger/src/schema/schema.controller.ts +++ b/apps/ledger/src/schema/schema.controller.ts @@ -1,59 +1,92 @@ import { Controller } from '@nestjs/common'; import { SchemaService } from './schema.service'; import { MessagePattern } from '@nestjs/microservices'; -import { ISchema, ISchemaCredDeffSearchInterface, ISchemaSearchPayload } from './interfaces/schema-payload.interface'; +import { + ISchema, + ISchemaCredDeffSearchInterface, + ISchemaExist, + ISchemaSearchPayload, + W3CSchemaPayload +} from './interfaces/schema-payload.interface'; import { schema } from '@prisma/client'; -import { ICredDefWithPagination, ISchemaData, ISchemasWithPagination } from '@credebl/common/interfaces/schema.interface'; - +import { + ICredDefWithPagination, + ISchemaData, + ISchemasWithPagination +} from '@credebl/common/interfaces/schema.interface'; @Controller('schema') export class SchemaController { - constructor(private readonly schemaService: SchemaService) { } - - @MessagePattern({ cmd: 'create-schema' }) - async createSchema(payload: ISchema): Promise { - const { schema, user, orgId } = payload; - return this.schemaService.createSchema(schema, user, orgId); - } - - @MessagePattern({ cmd: 'get-schema-by-id' }) - async getSchemaById(payload: ISchema): Promise { - const { schemaId, orgId } = payload; - return this.schemaService.getSchemaById(schemaId, orgId); - } - - @MessagePattern({ cmd: 'get-schemas' }) - async getSchemas(schemaSearch: ISchemaSearchPayload): Promise { - const { schemaSearchCriteria, orgId } = schemaSearch; - return this.schemaService.getSchemas(schemaSearchCriteria, orgId); - } - - @MessagePattern({ cmd: 'get-cred-deff-list-by-schemas-id' }) - async getcredDeffListBySchemaId(payload: ISchemaCredDeffSearchInterface): Promise { - return this.schemaService.getcredDeffListBySchemaId(payload); - } - - @MessagePattern({ cmd: 'get-all-schemas' }) - async getAllSchema(schemaSearch: ISchemaSearchPayload): Promise<{ - totalItems: number; - hasNextPage: boolean; - hasPreviousPage: boolean; - nextPage: number; - previousPage: number; - lastPage: number; - data: { - createDateTime: Date; - createdBy: string; - name: string; - schemaLedgerId: string; - version: string; - attributes: string; - publisherDid: string; - issuerId: string; - }[]; - }> { - const { schemaSearchCriteria } = schemaSearch; - return this.schemaService.getAllSchema(schemaSearchCriteria); - } + constructor(private readonly schemaService: SchemaService) {} + + @MessagePattern({ cmd: 'create-schema' }) + async createSchema(payload: ISchema): Promise { + const { schema, user, orgId } = payload; + return this.schemaService.createSchema(schema, user, orgId); + } + + @MessagePattern({ cmd: 'create-w3c-schema' }) + async createW3CSchema(payload: W3CSchemaPayload): Promise { + const {orgId, schemaPayload} = payload; + return this.schemaService.createW3CSchema(orgId, schemaPayload); + } + + @MessagePattern({ cmd: 'get-schema-by-id' }) + async getSchemaById(payload: ISchema): Promise { + const { schemaId, orgId } = payload; + return this.schemaService.getSchemaById(schemaId, orgId); + } + + @MessagePattern({ cmd: 'get-schemas' }) + async getSchemas(schemaSearch: ISchemaSearchPayload): Promise { + const { schemaSearchCriteria, orgId } = schemaSearch; + return this.schemaService.getSchemas(schemaSearchCriteria, orgId); + } + + @MessagePattern({ cmd: 'get-cred-deff-list-by-schemas-id' }) + async getcredDeffListBySchemaId(payload: ISchemaCredDeffSearchInterface): Promise { + return this.schemaService.getcredDeffListBySchemaId(payload); + } + + @MessagePattern({ cmd: 'get-all-schemas' }) + async getAllSchema(schemaSearch: ISchemaSearchPayload): Promise<{ + totalItems: number; + hasNextPage: boolean; + hasPreviousPage: boolean; + nextPage: number; + previousPage: number; + lastPage: number; + data: { + createDateTime: Date; + createdBy: string; + name: string; + schemaLedgerId: string; + version: string; + attributes: string; + publisherDid: string; + issuerId: string; + }[]; + }> { + const { schemaSearchCriteria } = schemaSearch; + return this.schemaService.getAllSchema(schemaSearchCriteria); + } + @MessagePattern({ cmd: 'schema-exist' }) + async schemaExist(payload: ISchemaExist): Promise<{ + id: string; + createDateTime: Date; + createdBy: string; + lastChangedDateTime: Date; + lastChangedBy: string; + name: string; + version: string; + attributes: string; + schemaLedgerId: string; + publisherDid: string; + issuerId: string; + orgId: string; + ledgerId: string; + }[]> { + return this.schemaService.schemaExist(payload); + } } diff --git a/apps/ledger/src/schema/schema.interface.ts b/apps/ledger/src/schema/schema.interface.ts index ad4f58ed3..3ce52edbb 100644 --- a/apps/ledger/src/schema/schema.interface.ts +++ b/apps/ledger/src/schema/schema.interface.ts @@ -23,6 +23,7 @@ export interface CreateSchemaAgentRedirection { agentType?: string; apiKey?: string; agentEndPoint?: string; + orgId?: string; } export interface ITenantSchemaDto { @@ -40,6 +41,7 @@ export interface GetSchemaAgentRedirection { agentEndPoint?: string; agentType?: string; method?: string; + orgId?: string; } export interface GetSchemaFromTenantPayload { diff --git a/apps/ledger/src/schema/schema.service.ts b/apps/ledger/src/schema/schema.service.ts index 8c967b9da..a5c71aa9c 100644 --- a/apps/ledger/src/schema/schema.service.ts +++ b/apps/ledger/src/schema/schema.service.ts @@ -11,7 +11,7 @@ import { ClientProxy, RpcException } from '@nestjs/microservices'; import { BaseService } from 'libs/service/base.service'; import { SchemaRepository } from './repositories/schema.repository'; import { schema } from '@prisma/client'; -import { ISchema, ISchemaCredDeffSearchInterface, ISchemaPayload, ISchemaSearchCriteria } from './interfaces/schema-payload.interface'; +import { ISchema, ISchemaCredDeffSearchInterface, ISchemaExist, ISchemaPayload, ISchemaSearchCriteria, SchemaPayload, W3CCreateSchema } from './interfaces/schema-payload.interface'; import { ResponseMessages } from '@credebl/common/response-messages'; import { IUserRequestInterface } from './interfaces/schema.interface'; import { CreateSchemaAgentRedirection, GetSchemaAgentRedirection } from './schema.interface'; @@ -36,13 +36,9 @@ export class SchemaService extends BaseService { schema: ISchemaPayload, user: IUserRequestInterface, orgId: string - ): Promise { + ): Promise { - let apiKey: string = await this.cacheService.get(CommonConstants.CACHE_APIKEY_KEY); - if (!apiKey || null === apiKey || undefined === apiKey) { - apiKey = await this._getOrgAgentApiKey(orgId); - } - const { userId } = user.selectedOrg; + const userId = user.id; try { const schemaExists = await this.schemaRepository.schemaExists( @@ -82,7 +78,8 @@ export class SchemaService extends BaseService { const trimmedAttributes = schema.attributes.map(attribute => ({ attributeName: attribute.attributeName.trim(), schemaDataType: attribute.schemaDataType, - displayName: attribute.displayName.trim() + displayName: attribute.displayName.trim(), + isRequired: attribute.isRequired })); @@ -122,7 +119,17 @@ export class SchemaService extends BaseService { const did = schema.orgDid?.split(':').length >= 4 ? schema.orgDid : orgDid; const orgAgentType = await this.schemaRepository.getOrgAgentType(getAgentDetails.org_agents[0].orgAgentTypeId); + const attributeArray = trimmedAttributes.map(item => item.attributeName); + + const isRequiredAttributeExists = trimmedAttributes.some(attribute => attribute.isRequired); + + if (!isRequiredAttributeExists) { + throw new BadRequestException( + ResponseMessages.schema.error.atLeastOneRequired + ); + } + let schemaResponseFromAgentService; if (OrgAgentType.DEDICATED === orgAgentType) { const issuerId = did; @@ -133,7 +140,7 @@ export class SchemaService extends BaseService { name: schema.schemaName, issuerId, agentEndPoint, - apiKey, + orgId, agentType: OrgAgentType.DEDICATED }; schemaResponseFromAgentService = await this._createSchema(schemaPayload); @@ -151,7 +158,7 @@ export class SchemaService extends BaseService { issuerId: did }, agentEndPoint, - apiKey, + orgId, agentType: OrgAgentType.SHARED }; schemaResponseFromAgentService = await this._createSchema(schemaPayload); @@ -235,6 +242,237 @@ export class SchemaService extends BaseService { } } + async createW3CSchema(orgId:string, schemaPayload: SchemaPayload): Promise { + try { + const { description, did, schemaAttributes, schemaName} = schemaPayload; + const agentDetails = await this.schemaRepository.getAgentDetailsByOrgId(orgId); + if (!agentDetails) { + throw new NotFoundException(ResponseMessages.schema.error.agentDetailsNotFound, { + cause: new Error(), + description: ResponseMessages.errorMessages.notFound + }); + } + const { agentEndPoint } = agentDetails; + const getAgentDetails = await this.schemaRepository.getAgentType(orgId); + const orgAgentType = await this.schemaRepository.getOrgAgentType(getAgentDetails.org_agents[0].orgAgentTypeId); + let url; + if (OrgAgentType.DEDICATED === orgAgentType) { + url = `${agentEndPoint}${CommonConstants.DEDICATED_CREATE_POLYGON_W3C_SCHEMA}`; + } else if (OrgAgentType.SHARED === orgAgentType) { + const { tenantId } = await this.schemaRepository.getAgentDetailsByOrgId(orgId); + url = `${agentEndPoint}${CommonConstants.SHARED_CREATE_POLYGON_W3C_SCHEMA}${tenantId}`; + } + + const schemaObject = await this.w3cSchemaBuilder(schemaAttributes, schemaName, description); + + if (!schemaObject) { + throw new BadRequestException(ResponseMessages.schema.error.schemaBuilder, { + cause: new Error(), + description: ResponseMessages.errorMessages.badRequest + }); + } + const agentSchemaPayload = { + schema:schemaObject, + did, + schemaName + }; + + const W3cSchemaPayload = { + url, + orgId, + schemaRequestPayload: agentSchemaPayload + }; + return this._createW3CSchema(W3cSchemaPayload); + } catch (error) { + this.logger.error(`[createSchema] - outer Error: ${JSON.stringify(error)}`); + throw new RpcException(error.error ? error.error.message : error.message); + } + } + + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type + private async w3cSchemaBuilder(schemaAttributes, schemaName: string, description: string) { + const schemaAttributeJson = schemaAttributes.map((attribute, index) => ({ + [attribute.title]: { + type: attribute.type.toLowerCase(), + order: index, + title: attribute.title + } + })); + + // Add the format property to the id key + schemaAttributeJson.unshift({ + id: { + type: 'string', + format: 'uri' + } + }); + + const nestedObject = {}; + schemaAttributeJson.forEach((obj) => { + // eslint-disable-next-line prefer-destructuring + const key = Object.keys(obj)[0]; + nestedObject[key] = obj[key]; + }); + + const schemaNameObject = {}; + schemaNameObject[schemaName] = { + "const": schemaName + }; + const date = new Date().toISOString(); + + const W3CSchema = { + $schema: 'http://json-schema.org/draft-07/schema#', + $id: `${date}-${schemaName}`, + type: 'object', + required: ['@context', 'issuer', 'issuanceDate', 'type', 'credentialSubject'], + properties: { + '@context': { + $ref: '#/definitions/context' + }, + type: { + type: 'array', + items: { + anyOf: [ + { + $ref: '#/definitions/VerifiableCredential' + }, + { + const: `#/definitions/$${schemaName}` + } + ] + } + }, + credentialSubject: { + $ref: '#/definitions/credentialSubject' + }, + id: { + type: 'string', + format: 'uri' + }, + issuer: { + $ref: '#/definitions/uriOrId' + }, + issuanceDate: { + type: 'string', + format: 'date-time' + }, + expirationDate: { + type: 'string', + format: 'date-time' + }, + credentialStatus: { + $ref: '#/definitions/credentialStatus' + }, + credentialSchema: { + $ref: '#/definitions/credentialSchema' + } + }, + definitions: { + context: { + type: 'array', + items: [ + { + const: 'https://www.w3.org/2018/credentials/v1' + } + ], + additionalItems: { + oneOf: [ + { + type: 'string', + format: 'uri' + }, + { + type: 'object' + }, + { + type: 'array', + items: { + $ref: '#/definitions/context' + } + } + ] + }, + minItems: 1, + uniqueItems: true + }, + credentialSubject: { + type: 'object', + required: ['id'], + additionalProperties: false, + properties: nestedObject + }, + VerifiableCredential: { + const: 'VerifiableCredential' + }, + credentialSchema: { + oneOf: [ + { + $ref: '#/definitions/idAndType' + }, + { + type: 'array', + items: { + $ref: '#/definitions/idAndType' + }, + minItems: 1, + uniqueItems: true + } + ] + }, + credentialStatus: { + oneOf: [ + { + $ref: '#/definitions/idAndType' + }, + { + type: 'array', + items: { + $ref: '#/definitions/idAndType' + }, + minItems: 1, + uniqueItems: true + } + ] + }, + idAndType: { + type: 'object', + required: ['id', 'type'], + properties: { + id: { + type: 'string', + format: 'uri' + }, + type: { + type: 'string' + } + } + }, + uriOrId: { + oneOf: [ + { + type: 'string', + format: 'uri' + }, + { + type: 'object', + required: ['id'], + properties: { + id: { + type: 'string', + format: 'uri' + } + } + } + ] + }, + ...schemaNameObject + }, + title: schemaName, + description: `${description}` + }; + return W3CSchema; + } + async _createSchema(payload: CreateSchemaAgentRedirection): Promise<{ response: string; }> { @@ -261,26 +499,40 @@ export class SchemaService extends BaseService { return schemaResponse; } + async _createW3CSchema(payload: W3CCreateSchema): Promise<{ + response: string; + }> { + const natsPattern = { + cmd: 'agent-create-w3c-schema' + }; + const W3CSchemaResponse = await this.schemaServiceProxy + .send(natsPattern, payload) + .pipe( + map((response) => ( + { + response + })) + ).toPromise() + .catch(error => { + this.logger.error(`Error in creating W3C schema : ${JSON.stringify(error)}`); + throw new RpcException(error.error ? error.error.message : error.message); + }); + return W3CSchemaResponse; + } + async getSchemaById(schemaId: string, orgId: string): Promise { try { const { agentEndPoint } = await this.schemaRepository.getAgentDetailsByOrgId(orgId); const getAgentDetails = await this.schemaRepository.getAgentType(orgId); const orgAgentType = await this.schemaRepository.getOrgAgentType(getAgentDetails.org_agents[0].orgAgentTypeId); - // const apiKey = ''; - - let apiKey; - apiKey = await this.cacheService.get(CommonConstants.CACHE_APIKEY_KEY); - if (!apiKey || null === apiKey || undefined === apiKey) { - apiKey = await this._getOrgAgentApiKey(orgId); - } let schemaResponse; if (OrgAgentType.DEDICATED === orgAgentType) { const getSchemaPayload = { schemaId, - apiKey, + orgId, agentEndPoint, agentType: OrgAgentType.DEDICATED }; @@ -293,7 +545,7 @@ export class SchemaService extends BaseService { payload: { schemaId }, agentType: OrgAgentType.SHARED, agentEndPoint, - apiKey + orgId }; schemaResponse = await this._getSchemaById(getSchemaPayload); } @@ -456,7 +708,7 @@ export class SchemaService extends BaseService { async _getOrgAgentApiKey(orgId: string): Promise { const pattern = { cmd: 'get-org-agent-api-key' }; const payload = { orgId }; - + try { // eslint-disable-next-line @typescript-eslint/no-explicit-any const message = await this.schemaServiceProxy.send(pattern, payload).toPromise(); @@ -470,5 +722,29 @@ export class SchemaService extends BaseService { } } + async schemaExist(payload: ISchemaExist): Promise<{ + id: string; + createDateTime: Date; + createdBy: string; + lastChangedDateTime: Date; + lastChangedBy: string; + name: string; + version: string; + attributes: string; + schemaLedgerId: string; + publisherDid: string; + issuerId: string; + orgId: string; + ledgerId: string; + }[]> { + try { + const schemaExist = await this.schemaRepository.schemaExist(payload); + return schemaExist; + + } catch (error) { + this.logger.error(`Error in schema exist: ${error}`); + throw new RpcException(error.response ? error.response : error); + } + } } diff --git a/apps/notification/interfaces/notification.interfaces.ts b/apps/notification/interfaces/notification.interfaces.ts new file mode 100644 index 000000000..57edecd46 --- /dev/null +++ b/apps/notification/interfaces/notification.interfaces.ts @@ -0,0 +1,23 @@ + + +export interface INotification { + id: string; + orgId: string; + notificationWebhook: string; +} + +export interface IWebhookEndpoint { + orgId: string; + notificationWebhook: string; +} + +export interface ISendNotification { + fcmToken: string; + messageType: string; + clientCode: string; +} + +export interface IGetNotification { + fcmToken: string; + messageType: string; +} \ No newline at end of file diff --git a/apps/notification/src/main.ts b/apps/notification/src/main.ts new file mode 100644 index 000000000..0b4b87bf8 --- /dev/null +++ b/apps/notification/src/main.ts @@ -0,0 +1,23 @@ +import { NestFactory } from '@nestjs/core'; +import { HttpExceptionFilter } from 'libs/http-exception.filter'; +import { Logger } from '@nestjs/common'; +import { MicroserviceOptions, Transport } from '@nestjs/microservices'; +import { NotificationModule } from '../src/notification.module'; +import { getNatsOptions } from '@credebl/common/nats.config'; + +const logger = new Logger(); + +async function bootstrap(): Promise { + + const app = await NestFactory.createMicroservice(NotificationModule, { + transport: Transport.NATS, + options: getNatsOptions(process.env.NOTIFICATION_NKEY_SEED) + + }); + + app.useGlobalFilters(new HttpExceptionFilter()); + + await app.listen(); + logger.log('Notification-Service Microservice is listening to NATS '); +} +bootstrap(); \ No newline at end of file diff --git a/apps/notification/src/notification.controller.ts b/apps/notification/src/notification.controller.ts new file mode 100644 index 000000000..40c9d33e8 --- /dev/null +++ b/apps/notification/src/notification.controller.ts @@ -0,0 +1,29 @@ +import { Controller } from '@nestjs/common'; +import { NotificationService } from './notification.service'; +import { MessagePattern } from '@nestjs/microservices'; +import { INotification, IWebhookEndpoint, ISendNotification } from '../interfaces/notification.interfaces'; + +@Controller() +export class NotificationController { + constructor(private readonly notificationService: NotificationService) {} + + /** + * Register organization webhook endpoint + * @param payload + * @returns Stored notification data + */ + @MessagePattern({ cmd: 'register-org-webhook-endpoint-for-notification' }) + async registerOrgWebhookEndpoint(payload: IWebhookEndpoint): Promise { + return this.notificationService.registerOrgWebhookEndpoint(payload); + } + + /** + * Send notification for holder + * @param payload + * @returns Get notification details + */ + @MessagePattern({ cmd: 'send-notification' }) + async sendNotification(payload: ISendNotification): Promise { + return this.notificationService.sendNotification(payload); + } +} \ No newline at end of file diff --git a/apps/notification/src/notification.module.ts b/apps/notification/src/notification.module.ts new file mode 100644 index 000000000..658d3a736 --- /dev/null +++ b/apps/notification/src/notification.module.ts @@ -0,0 +1,28 @@ +import { CommonModule } from '@credebl/common'; +import { getNatsOptions } from '@credebl/common/nats.config'; +import { CacheModule } from '@nestjs/cache-manager'; +import { Logger, Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { ClientsModule, Transport } from '@nestjs/microservices'; +import { NotificationController } from './notification.controller'; +import { NotificationService } from './notification.service'; +import { PrismaService } from '@credebl/prisma-service'; +import { NotificationRepository } from './notification.repository'; + +@Module({ + imports: [ + ConfigModule.forRoot(), + ClientsModule.register([ + { + name: 'NATS_CLIENT', + transport: Transport.NATS, + options: getNatsOptions(process.env.NOTIFICATION_NKEY_SEED) + } + ]), + CommonModule, + CacheModule.register({ host: process.env.REDIS_HOST, port: process.env.REDIS_PORT }) + ], + controllers: [NotificationController], + providers: [NotificationService, NotificationRepository, PrismaService, Logger] +}) +export class NotificationModule { } \ No newline at end of file diff --git a/apps/notification/src/notification.repository.ts b/apps/notification/src/notification.repository.ts new file mode 100644 index 000000000..e543899b7 --- /dev/null +++ b/apps/notification/src/notification.repository.ts @@ -0,0 +1,65 @@ +import { ResponseMessages } from '@credebl/common/response-messages'; +import { PrismaService } from '@credebl/prisma-service'; +import { Injectable, Logger, NotFoundException } from '@nestjs/common'; +import { INotification, IWebhookEndpoint } from '../interfaces/notification.interfaces'; + +@Injectable() +export class NotificationRepository { + constructor( + private readonly prisma: PrismaService, + private readonly logger: Logger + ) { } + + + /** + * Register organization webhook endpoint + * @param payload + * @returns Stored notification data + */ + async storeOrgWebhookEndpoint(payload: IWebhookEndpoint): Promise { + try { + + const { orgId, notificationWebhook } = payload; + const updateNotification = await this.prisma.notification.create({ + data: { + orgId, + notificationWebhook + } + }); + + if (!updateNotification) { + throw new NotFoundException(ResponseMessages.notification.error.notFound); + } + + return updateNotification; + } catch (error) { + this.logger.error(`Error in storeOrgWebhookEndpoint: ${error.message} `); + throw error; + } + } + + /** + * Get webhook endpoint + * @param orgId + * @returns Get notification details + */ + async getOrgWebhookEndpoint(orgId: string): Promise { + try { + + const updateNotification = await this.prisma.notification.findUnique({ + where: { + orgId + } + }); + + if (!updateNotification) { + throw new NotFoundException(ResponseMessages.notification.error.notFound); + } + + return updateNotification; + } catch (error) { + this.logger.error(`Error in getOrgWebhookEndpoint: ${error.message} `); + throw error; + } + } +} \ No newline at end of file diff --git a/apps/notification/src/notification.service.ts b/apps/notification/src/notification.service.ts new file mode 100644 index 000000000..01bb467d2 --- /dev/null +++ b/apps/notification/src/notification.service.ts @@ -0,0 +1,83 @@ +import { BadRequestException, Injectable, Logger } from '@nestjs/common'; +import { + INotification, + IWebhookEndpoint, + ISendNotification +} from '../interfaces/notification.interfaces'; +import { RpcException } from '@nestjs/microservices'; +import { NotificationRepository } from './notification.repository'; +import { ResponseMessages } from '@credebl/common/response-messages'; +import { CommonService } from '@credebl/common'; + +@Injectable() +export class NotificationService { + private readonly logger = new Logger('NotificationService'); + constructor( + private readonly commonService: CommonService, + private readonly notificationRepository: NotificationRepository + ) {} + + /** + * Register organization webhook endpoint + * @param payload + * @returns Stored notification data + */ + async registerOrgWebhookEndpoint(payload: IWebhookEndpoint): Promise { + try { + /** + * Call the function for store the org webhook endpoint on notification table + */ + const storeOrgWebhookEndpoint = await this.notificationRepository.storeOrgWebhookEndpoint(payload); + return storeOrgWebhookEndpoint; + } catch (error) { + this.logger.error(`[registerEndpoint] - error in register org webhook endpoint: ${JSON.stringify(error)}`); + throw new RpcException(error.response ? error.response : error); + } + } + + /** + * Send notification for holder + * @param payload + * @returns Get notification details + */ + async sendNotification(payload: ISendNotification): Promise { + try { + const orgId = payload.clientCode; + + /** + * Fetch the webhook endpoint by orgId + */ + const getWebhookUrl = await this.notificationRepository.getOrgWebhookEndpoint(orgId); + + const webhookPayload = { + fcmToken: payload.fcmToken, + messageType: payload.messageType + }; + + /** + * Send notification details with webhook endpoint + */ + const webhookResponse = await this.commonService + .httpPost(getWebhookUrl?.notificationWebhook, webhookPayload) + .then(async (response) => response) + .catch((error) => { + this.logger.error(`Error in sendNotification : ${JSON.stringify(error)}`); + throw error; + }); + + if (!this.isValidUrl(getWebhookUrl?.notificationWebhook)) { + throw new BadRequestException(ResponseMessages.notification.error.invalidUrl); + } + + return webhookResponse; + } catch (error) { + this.logger.error(`[registerEndpoint] - error in send notification: ${JSON.stringify(error)}`); + throw new RpcException(error.response ? error.response : error); + } + } + + private isValidUrl(url: string): boolean { + const urlRegex = /^(https?|ftp):\/\/[^\s/$.?#].[^\s]*$/; + return urlRegex.test(url); + } +} diff --git a/apps/notification/tsconfig.app.json b/apps/notification/tsconfig.app.json new file mode 100644 index 000000000..87277fc96 --- /dev/null +++ b/apps/notification/tsconfig.app.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "declaration": false, + "outDir": "../../dist/apps/notification" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] +} diff --git a/apps/organization/dtos/create-organization.dto.ts b/apps/organization/dtos/create-organization.dto.ts index 0897f9a30..9fbdd8d15 100644 --- a/apps/organization/dtos/create-organization.dto.ts +++ b/apps/organization/dtos/create-organization.dto.ts @@ -10,6 +10,7 @@ export class CreateOrganizationDto { createdBy?:string; updatedBy?:string; lastChangedBy?:string; + notificationWebhook?: string; } export class CreateUserRoleOrgDto { diff --git a/apps/organization/dtos/update-invitation.dt.ts b/apps/organization/dtos/update-invitation.dt.ts index 1d80b08d4..21e345858 100644 --- a/apps/organization/dtos/update-invitation.dt.ts +++ b/apps/organization/dtos/update-invitation.dt.ts @@ -5,5 +5,6 @@ export class UpdateInvitationDto { orgId: string; status: Invitation; userId: string; + keycloakUserId: string; email: string; } \ No newline at end of file diff --git a/apps/organization/interfaces/organization.interface.ts b/apps/organization/interfaces/organization.interface.ts index adbd4f8f2..032dd76ac 100644 --- a/apps/organization/interfaces/organization.interface.ts +++ b/apps/organization/interfaces/organization.interface.ts @@ -23,6 +23,18 @@ export interface IUpdateOrganization { } +export interface ICreateConnectionUrl { + id: string; + orgId: string; + agentId: string; + connectionInvitation: string; + multiUse: boolean; + createDateTime: Date; + createdBy: number; + lastChangedDateTime: Date; + lastChangedBy: number; +} + export interface IOrgAgent { url: string; apiKey: string; diff --git a/apps/organization/repositories/organization.repository.ts b/apps/organization/repositories/organization.repository.ts index 2c37e07c7..240969af3 100644 --- a/apps/organization/repositories/organization.repository.ts +++ b/apps/organization/repositories/organization.repository.ts @@ -3,12 +3,12 @@ import { Injectable, Logger, NotFoundException } from '@nestjs/common'; // eslint-disable-next-line camelcase -import { org_agents, org_invitations, user_org_roles } from '@prisma/client'; +import { Prisma, agent_invitations, org_agents, org_invitations, user_org_roles } from '@prisma/client'; import { CreateOrganizationDto } from '../dtos/create-organization.dto'; import { IGetOrgById, IGetOrganization, IUpdateOrganization } from '../interfaces/organization.interface'; import { InternalServerErrorException } from '@nestjs/common'; -import { Invitation } from '@credebl/enum/enum'; +import { Invitation, SortValue } from '@credebl/enum/enum'; import { PrismaService } from '@credebl/prisma-service'; import { UserOrgRolesService } from '@credebl/user-org-roles'; import { organisation } from '@prisma/client'; @@ -38,7 +38,21 @@ export class OrganizationRepository { }); } catch (error) { this.logger.error(`error: ${JSON.stringify(error)}`); - throw new InternalServerErrorException(error); + throw error; + } + } + + + async checkOrganizationSlugExist(orgSlug: string): Promise { + try { + return this.prisma.organisation.findUnique({ + where: { + orgSlug + } + }); + } catch (error) { + this.logger.error(`error in checkOrganizationSlugExist: ${JSON.stringify(error)}`); + throw error; } } @@ -97,6 +111,35 @@ export class OrganizationRepository { } } + async getAgentInvitationDetails(orgId: string): Promise { + try { + const response = await this.prisma.agent_invitations.findUnique({ + where: { + id: orgId + } + }); + return response; + } catch (error) { + this.logger.error(`error in getting agent invitation details: ${JSON.stringify(error)}`); + throw error; + } + } + + async updateConnectionInvitationDetails(orgId: string, connectionInvitation: string): Promise { + try { + const temp = await this.prisma.agent_invitations.updateMany({ + where: {orgId}, + data: { + connectionInvitation + } + }); + return temp; + + } catch (error) { + this.logger.error(`Error in updating connection invitation details: ${JSON.stringify(error)}`); + throw error; + } + } /** * @@ -348,6 +391,7 @@ export class OrganizationRepository { select: { id: true, orgDid: true, + didDocument: true, walletName: true, agentEndPoint: true, agentSpinUpStatus: true, @@ -449,6 +493,38 @@ export class OrganizationRepository { } } + async getUnregisteredClientOrgs(): Promise { + try { + const recordsWithNullIdpId = await this.prisma.organisation.findMany({ + where: { + idpId: null + }, + include: { + userOrgRoles: { + include: { + user: { + select: { + email: true, + username: true, + id: true, + keycloakUserId: true, + isEmailVerified: true + } + }, + orgRole: true + } + } + } + }); + + return recordsWithNullIdpId; + + } catch (error) { + this.logger.error(`error: ${JSON.stringify(error)}`); + throw error; + } + } + /** * * @param queryObject @@ -478,7 +554,7 @@ export class OrganizationRepository { pageSize: number ): Promise { try { - const sortByName = 'asc'; + const sortByName = SortValue.DESC; const result = await this.prisma.$transaction([ this.prisma.organisation.findMany({ where: { @@ -538,9 +614,9 @@ export class OrganizationRepository { * @returns Organization exist details */ - async checkOrganizationExist(name: string, orgId: string): Promise { + async checkOrganizationExist(name: string, orgId: string): Promise { try { - return this.prisma.organisation.findMany({ + return this.prisma.organisation.findUnique({ where: { id: orgId, name @@ -548,7 +624,7 @@ export class OrganizationRepository { }); } catch (error) { this.logger.error(`error: ${JSON.stringify(error)}`); - throw new InternalServerErrorException(error); + throw error; } } diff --git a/apps/organization/src/organization.controller.ts b/apps/organization/src/organization.controller.ts index 87b1c45ea..245f4a8e8 100644 --- a/apps/organization/src/organization.controller.ts +++ b/apps/organization/src/organization.controller.ts @@ -8,9 +8,9 @@ import { BulkSendInvitationDto } from '../dtos/send-invitation.dto'; import { UpdateInvitationDto } from '../dtos/update-invitation.dt'; import { IGetOrgById, IGetOrganization, IUpdateOrganization, Payload } from '../interfaces/organization.interface'; import { organisation } from '@prisma/client'; -import { IOrgRoles } from 'libs/org-roles/interfaces/org-roles.interface'; import { IOrgCredentials, IOrganizationInvitations, IOrganization, IOrganizationDashboard } from '@credebl/common/interfaces/organization.interface'; import { IAccessTokenData } from '@credebl/common/interfaces/interface'; +import { IClientRoles } from '@credebl/client-registration/interfaces/client.interface'; @Controller() export class OrganizationController { @@ -24,8 +24,8 @@ export class OrganizationController { */ @MessagePattern({ cmd: 'create-organization' }) - async createOrganization(@Body() payload: { createOrgDto: CreateOrganizationDto; userId: string }): Promise { - return this.organizationService.createOrganization(payload.createOrgDto, payload.userId); + async createOrganization(@Body() payload: { createOrgDto: CreateOrganizationDto; userId: string, keycloakUserId: string }): Promise { + return this.organizationService.createOrganization(payload.createOrgDto, payload.userId, payload.keycloakUserId); } /** @@ -34,8 +34,8 @@ export class OrganizationController { * @returns organization client credentials */ @MessagePattern({ cmd: 'create-org-credentials' }) - async createOrgCredentials(@Body() payload: { orgId: string; userId: string }): Promise { - return this.organizationService.createOrgCredentials(payload.orgId); + async createOrgCredentials(@Body() payload: { orgId: string; userId: string, keycloakUserId: string }): Promise { + return this.organizationService.createOrgCredentials(payload.orgId, payload.userId, payload.keycloakUserId); } /** @@ -113,8 +113,13 @@ export class OrganizationController { */ @MessagePattern({ cmd: 'get-org-roles' }) - async getOrgRoles(): Promise { - return this.organizationService.getOrgRoles(); + async getOrgRoles(payload: {orgId: string}): Promise { + return this.organizationService.getOrgRoles(payload.orgId); + } + + @MessagePattern({ cmd: 'register-orgs-users-map' }) + async registerOrgsMapUsers(): Promise { + return this.organizationService.registerOrgsMapUsers(); } /** diff --git a/apps/organization/src/organization.service.ts b/apps/organization/src/organization.service.ts index aa2d580cf..bea77a66e 100644 --- a/apps/organization/src/organization.service.ts +++ b/apps/organization/src/organization.service.ts @@ -1,6 +1,16 @@ /* eslint-disable prefer-destructuring */ -import { organisation, user } from '@prisma/client'; -import { Injectable, Logger, ConflictException, InternalServerErrorException, HttpException, BadRequestException, ForbiddenException } from '@nestjs/common'; +// eslint-disable-next-line camelcase +import { org_invitations, organisation, user } from '@prisma/client'; +import { + Injectable, + Logger, + ConflictException, + InternalServerErrorException, + HttpException, + BadRequestException, + ForbiddenException, + UnauthorizedException +} from '@nestjs/common'; import { PrismaService } from '@credebl/prisma-service'; import { CommonService } from '@credebl/common'; import { OrganizationRepository } from '../repositories/organization.repository'; @@ -17,7 +27,7 @@ import { CreateOrganizationDto } from '../dtos/create-organization.dto'; import { BulkSendInvitationDto } from '../dtos/send-invitation.dto'; import { UpdateInvitationDto } from '../dtos/update-invitation.dt'; import { Invitation, OrgAgentType, transition } from '@credebl/enum/enum'; -import { IGetOrgById, IGetOrganization, IUpdateOrganization, IOrgAgent, IClientCredentials } from '../interfaces/organization.interface'; +import { IGetOrgById, IGetOrganization, IUpdateOrganization, IOrgAgent, IClientCredentials, ICreateConnectionUrl, IOrgRole } from '../interfaces/organization.interface'; import { UserActivityService } from '@credebl/user-activity'; import { CommonConstants } from '@credebl/common/common.constant'; import { ClientRegistrationService } from '@credebl/client-registration/client-registration.service'; @@ -25,11 +35,16 @@ import { map } from 'rxjs/operators'; import { Cache } from 'cache-manager'; import { AwsService } from '@credebl/aws'; import { CACHE_MANAGER } from '@nestjs/cache-manager'; -import { IOrgRoles } from 'libs/org-roles/interfaces/org-roles.interface'; -import { IOrgCredentials, IOrganization, IOrganizationInvitations, IOrganizationDashboard } from '@credebl/common/interfaces/organization.interface'; +import { + IOrgCredentials, + IOrganization, + IOrganizationInvitations, + IOrganizationDashboard +} from '@credebl/common/interfaces/organization.interface'; import { ClientCredentialTokenPayloadDto } from '@credebl/client-registration/dtos/client-credential-token-payload.dto'; import { IAccessTokenData } from '@credebl/common/interfaces/interface'; +import { IClientRoles } from '@credebl/client-registration/interfaces/client.interface'; @Injectable() export class OrganizationService { constructor( @@ -44,7 +59,7 @@ export class OrganizationService { private readonly logger: Logger, @Inject(CACHE_MANAGER) private cacheService: Cache, private readonly clientRegistrationService: ClientRegistrationService - ) { } + ) {} /** * @@ -53,9 +68,12 @@ export class OrganizationService { */ // eslint-disable-next-line camelcase - async createOrganization(createOrgDto: CreateOrganizationDto, userId: string): Promise { + async createOrganization( + createOrgDto: CreateOrganizationDto, + userId: string, + keycloakUserId: string + ): Promise { try { - const organizationExist = await this.organizationRepository.checkOrganizationNameExist(createOrgDto.name); if (organizationExist) { @@ -63,6 +81,13 @@ export class OrganizationService { } const orgSlug = this.createOrgSlug(createOrgDto.name); + + const isOrgSlugExist = await this.organizationRepository.checkOrganizationSlugExist(orgSlug); + + if (isOrgSlugExist) { + throw new ConflictException(ResponseMessages.organisation.error.exists); + } + createOrgDto.orgSlug = orgSlug; createOrgDto.createdBy = userId; createOrgDto.lastChangedBy = userId; @@ -73,7 +98,7 @@ export class OrganizationService { } else { createOrgDto.logo = ''; } - + const organizationDetails = await this.organizationRepository.createOrganization(createOrgDto); // To return selective object data @@ -82,14 +107,47 @@ export class OrganizationService { delete organizationDetails.orgSlug; delete organizationDetails.website; - const ownerRoleData = await this.orgRoleService.getRole(OrgRoles.OWNER); + try { + const orgCredentials = await this.registerToKeycloak( + organizationDetails.name, + organizationDetails.id, + keycloakUserId, + userId, + false + ); - await this.userOrgRoleService.createUserOrgRole(userId, ownerRoleData.id, organizationDetails.id); + const { clientId, idpId } = orgCredentials; - await this.userActivityService.createActivity(userId, organizationDetails.id, `${organizationDetails.name} organization created`, 'Get started with inviting users to join organization'); - - return organizationDetails; + const updateOrgData = { + clientId, + idpId + }; + + const updatedOrg = await this.organizationRepository.updateOrganizationById( + updateOrgData, + organizationDetails.id + ); + + if (!updatedOrg) { + throw new InternalServerErrorException(ResponseMessages.organisation.error.credentialsNotUpdate); + } + } catch (error) { + this.logger.error(`Error In creating client : ${JSON.stringify(error)}`); + throw new InternalServerErrorException('Unable to create client'); + } + + if (createOrgDto.notificationWebhook) { + await this.storeOrgWebhookEndpoint(organizationDetails.id, createOrgDto.notificationWebhook); + } + + await this.userActivityService.createActivity( + userId, + organizationDetails.id, + `${organizationDetails.name} organization created`, + 'Get started with inviting users to join organization' + ); + return organizationDetails; } catch (error) { this.logger.error(`In create organization : ${JSON.stringify(error)}`); throw new RpcException(error.response ? error.response : error); @@ -97,28 +155,57 @@ export class OrganizationService { } /** - * - * @param orgId + * + * @param orgId * @returns organization client credentials */ - async createOrgCredentials(orgId: string): Promise { + async createOrgCredentials(orgId: string, userId: string, keycloakUserId: string): Promise { try { - const organizationDetails = await this.organizationRepository.getOrganizationDetails(orgId); if (!organizationDetails) { throw new ConflictException(ResponseMessages.organisation.error.orgNotFound); } - const orgCredentials = await this.registerToKeycloak(organizationDetails.name, organizationDetails.id); - - const {clientId, clientSecret, idpId} = orgCredentials; + let updateOrgData = {}; + let generatedClientSecret = ''; - const updateOrgData = { - clientId, - clientSecret: this.maskString(clientSecret), - idpId - }; + if (organizationDetails.idpId) { + const token = await this.clientRegistrationService.getManagementToken(); + + generatedClientSecret = await this.clientRegistrationService.generateClientSecret( + organizationDetails.idpId, + token + ); + + updateOrgData = { + clientSecret: this.maskString(generatedClientSecret) + }; + } else { + + try { + const orgCredentials = await this.registerToKeycloak( + organizationDetails.name, + organizationDetails.id, + keycloakUserId, + userId, + true + ); + + const { clientId, idpId, clientSecret } = orgCredentials; + + generatedClientSecret = clientSecret; + + updateOrgData = { + clientId, + clientSecret: this.maskString(clientSecret), + idpId + }; + } catch (error) { + this.logger.error(`Error In creating client : ${JSON.stringify(error)}`); + throw new InternalServerErrorException('Unable to create client'); + } + } const updatedOrg = await this.organizationRepository.updateOrganizationById(updateOrgData, orgId); @@ -126,8 +213,11 @@ export class OrganizationService { throw new InternalServerErrorException(ResponseMessages.organisation.error.credentialsNotUpdate); } - return orgCredentials; - + return { + idpId: updatedOrg.idpId, + clientId: updatedOrg.clientId, + clientSecret: generatedClientSecret + }; } catch (error) { this.logger.error(`In createOrgCredentials : ${JSON.stringify(error)}`); throw new RpcException(error.response ? error.response : error); @@ -136,46 +226,95 @@ export class OrganizationService { /** * Register the organization to keycloak - * @param orgName - * @param orgId + * @param orgName + * @param orgId * @returns client credentials */ - async registerToKeycloak(orgName: string, orgId: string): Promise { - const token = await this.clientRegistrationService.getManagementToken(); - return this.clientRegistrationService.createClient(orgName, orgId, token); - } + async registerToKeycloak( + orgName: string, + orgId: string, + keycloakUserId: string, + userId: string, + shouldUpdateRole: boolean + ): Promise { + const token = await this.clientRegistrationService.getManagementToken(); + const orgDetails = await this.clientRegistrationService.createClient(orgName, orgId, token); + + const orgRolesList = [OrgRoles.OWNER, OrgRoles.ADMIN, OrgRoles.ISSUER, OrgRoles.VERIFIER, OrgRoles.MEMBER]; + + for (const role of orgRolesList) { + await this.clientRegistrationService.createClientRole(orgDetails.idpId, token, role, role); + } + + const ownerRoleClient = await this.clientRegistrationService.getClientSpecificRoles( + orgDetails.idpId, + token, + OrgRoles.OWNER + ); + + const payload = [ + { + id: ownerRoleClient.id, + name: ownerRoleClient.name + } + ]; + + const ownerRoleData = await this.orgRoleService.getRole(OrgRoles.OWNER); + + if (!shouldUpdateRole) { + await Promise.all([ + this.clientRegistrationService.createUserClientRole(orgDetails.idpId, token, keycloakUserId, payload), + this.userOrgRoleService.createUserOrgRole(userId, ownerRoleData.id, orgId, ownerRoleClient.id) + ]); + + } else { + const roleIdList = [ + { + roleId: ownerRoleData.id, + idpRoleId: ownerRoleClient.id + } + ]; + + await Promise.all([ + this.clientRegistrationService.createUserClientRole(orgDetails.idpId, token, keycloakUserId, payload), + this.userOrgRoleService.deleteOrgRoles(userId, orgId), + this.userOrgRoleService.updateUserOrgRole(userId, orgId, roleIdList) + ]); + } + + return orgDetails; + } async deleteClientCredentials(orgId: string): Promise { - const token = await this.clientRegistrationService.getManagementToken(); + const token = await this.clientRegistrationService.getManagementToken(); - const organizationDetails = await this.organizationRepository.getOrganizationDetails(orgId); + const organizationDetails = await this.organizationRepository.getOrganizationDetails(orgId); - if (!organizationDetails) { - throw new NotFoundException(ResponseMessages.organisation.error.orgNotFound); - } + if (!organizationDetails) { + throw new NotFoundException(ResponseMessages.organisation.error.orgNotFound); + } - try { - await this.clientRegistrationService.deleteClient(organizationDetails.idpId, token); - const updateOrgData = { - clientId: null, - clientSecret: null, - idpId: null - }; - - await this.organizationRepository.updateOrganizationById(updateOrgData, orgId); - - } catch (error) { - throw new InternalServerErrorException('Unable to delete client credentails'); - } + try { + await this.clientRegistrationService.deleteClient(organizationDetails.idpId, token); + const updateOrgData = { + clientId: null, + clientSecret: null, + idpId: null + }; + + await this.organizationRepository.updateOrganizationById(updateOrgData, orgId); + } catch (error) { + throw new InternalServerErrorException('Unable to delete client credentails'); + } - return ResponseMessages.organisation.success.deleteCredentials; + return ResponseMessages.organisation.success.deleteCredentials; } /** * Mask string and display last 5 characters - * @param inputString - * @returns + * @param inputString + * @returns */ maskString(inputString: string): string { if (5 <= inputString.length) { @@ -191,20 +330,20 @@ export class OrganizationService { return inputString; } } - - async isValidBase64 (value: string): Promise { + + async isValidBase64(value: string): Promise { try { if (!value || 'string' !== typeof value) { return false; } - + const base64Regex = /^data:image\/([a-zA-Z]*);base64,([^\"]*)$/; const matches = value.match(base64Regex); return Boolean(matches) && 3 === matches.length; } catch (error) { return false; } - }; + } async uploadFileToS3(orgLogo: string): Promise { try { @@ -223,8 +362,8 @@ export class OrganizationService { this.logger.error(`In getting imageUrl : ${JSON.stringify(error)}`); throw new RpcException(error.response ? error.response : error); } - } - + } + /** * * @param orgName @@ -248,19 +387,16 @@ export class OrganizationService { async updateOrganization(updateOrgDto: IUpdateOrganization, userId: string, orgId: string): Promise { try { - const organizationExist = await this.organizationRepository.checkOrganizationExist(updateOrgDto.name, orgId); + const organizationExist = await this.organizationRepository.checkOrganizationNameExist(updateOrgDto.name); - if (0 === organizationExist.length) { - const organizationExist = await this.organizationRepository.checkOrganizationNameExist(updateOrgDto.name); - if (organizationExist) { - throw new ConflictException(ResponseMessages.organisation.error.exists); - } + if (organizationExist && organizationExist.id !== orgId) { + throw new ConflictException(ResponseMessages.organisation.error.exists); } const orgSlug = await this.createOrgSlug(updateOrgDto.name); updateOrgDto.orgSlug = orgSlug; updateOrgDto.userId = userId; - + if (await this.isValidBase64(updateOrgDto.logo)) { const imageUrl = await this.uploadFileToS3(updateOrgDto.logo); updateOrgDto.logo = imageUrl; @@ -268,7 +404,16 @@ export class OrganizationService { delete updateOrgDto.logo; } - const organizationDetails = await this.organizationRepository.updateOrganization(updateOrgDto); + let organizationDetails; + const checkAgentIsExists = await this.organizationRepository.getAgentInvitationDetails(orgId); + + if (!checkAgentIsExists?.connectionInvitation && !checkAgentIsExists?.agentId) { + organizationDetails = await this.organizationRepository.updateOrganization(updateOrgDto); + } else if (organizationDetails?.logoUrl !== organizationExist?.logoUrl || organizationDetails?.name !== organizationExist?.name) { + const invitationData = await this._createConnection(updateOrgDto?.logo, updateOrgDto?.name, orgId); + await this.organizationRepository.updateConnectionInvitationDetails(orgId, invitationData?.connectionInvitation); + } + await this.userActivityService.createActivity(userId, organizationDetails.id, `${organizationDetails.name} organization updated`, 'Organization details updated successfully'); return organizationDetails; } catch (error) { @@ -277,13 +422,47 @@ export class OrganizationService { } } + + async _createConnection( + orgName: string, + logoUrl: string, + orgId: string + ): Promise { + const pattern = { cmd: 'create-connection' }; + + const payload = { + orgName, + logoUrl, + orgId + }; + const connectionInvitationData = await this.organizationServiceProxy + .send(pattern, payload) + .toPromise() + .catch((error) => { + this.logger.error(`catch: ${JSON.stringify(error)}`); + throw new HttpException( + { + status: error.status, + error: error.message + }, + error.status + ); + }); + + return connectionInvitationData; + } + /** * @returns Get created organizations details */ - async getOrganizations(userId: string, pageNumber: number, pageSize: number, search: string): Promise { + async getOrganizations( + userId: string, + pageNumber: number, + pageSize: number, + search: string + ): Promise { try { - const query = { userOrgRoles: { some: { userId } @@ -298,14 +477,8 @@ export class OrganizationService { userId }; - const getOrgs = await this.organizationRepository.getOrganizations( - query, - filterOptions, - pageNumber, - pageSize - ); + const getOrgs = await this.organizationRepository.getOrganizations(query, filterOptions, pageNumber, pageSize); return getOrgs; - } catch (error) { this.logger.error(`In fetch getOrganizations : ${JSON.stringify(error)}`); throw new RpcException(error.response ? error.response : error); @@ -313,31 +486,30 @@ export class OrganizationService { } async clientLoginCredentails(clientCredentials: IClientCredentials): Promise { - - const {clientId, clientSecret} = clientCredentials; - return this.authenticateClientKeycloak(clientId, clientSecret); - } - + const {clientId, clientSecret} = clientCredentials; + return this.authenticateClientKeycloak(clientId, clientSecret); +} async authenticateClientKeycloak(clientId: string, clientSecret: string): Promise { - + try { + const payload = new ClientCredentialTokenPayloadDto(); + // eslint-disable-next-line camelcase + payload.client_id = clientId; + // eslint-disable-next-line camelcase + payload.client_secret = clientSecret; - const payload = new ClientCredentialTokenPayloadDto(); - // eslint-disable-next-line camelcase - payload.client_id = clientId; - // eslint-disable-next-line camelcase - payload.client_secret = clientSecret; - payload.scope = 'email profile'; - + try { const mgmtTokenResponse = await this.clientRegistrationService.getToken(payload); return mgmtTokenResponse; + } catch (error) { + throw new UnauthorizedException(ResponseMessages.organisation.error.invalidClient); + } } catch (error) { this.logger.error(`Error in authenticateClientKeycloak : ${JSON.stringify(error)}`); throw new RpcException(error.response ? error.response : error); } - } /** @@ -348,7 +520,6 @@ export class OrganizationService { async getPublicOrganizations(pageNumber: number, pageSize: number, search: string): Promise { try { - const query = { publicProfile: true, OR: [ @@ -359,13 +530,7 @@ export class OrganizationService { const filterOptions = {}; - return this.organizationRepository.getOrganizations( - query, - filterOptions, - pageNumber, - pageSize - ); - + return this.organizationRepository.getOrganizations(query, filterOptions, pageNumber, pageSize); } catch (error) { this.logger.error(`In fetch getPublicOrganizations : ${JSON.stringify(error)}`); throw new RpcException(error.response ? error.response : error); @@ -375,7 +540,6 @@ export class OrganizationService { async getPublicProfile(payload: { orgSlug: string }): Promise { const { orgSlug } = payload; try { - const query = { orgSlug, publicProfile: true @@ -389,7 +553,6 @@ export class OrganizationService { const credDefs = await this.organizationRepository.getCredDefByOrg(organizationDetails.id); organizationDetails['credential_definitions'] = credDefs; return organizationDetails; - } catch (error) { this.logger.error(`get user: ${JSON.stringify(error)}`); throw new RpcException(error.response ? error.response : error); @@ -404,7 +567,6 @@ export class OrganizationService { async getOrganization(orgId: string): Promise { try { - const query = { id: orgId }; @@ -423,13 +585,23 @@ export class OrganizationService { * @returns Get created invitation details */ - async getInvitationsByOrgId(orgId: string, pageNumber: number, pageSize: number, search: string): Promise { + async getInvitationsByOrgId( + orgId: string, + pageNumber: number, + pageSize: number, + search: string + ): Promise { try { - const getOrganization = await this.organizationRepository.getInvitationsByOrgId(orgId, pageNumber, pageSize, search); + const getOrganization = await this.organizationRepository.getInvitationsByOrgId( + orgId, + pageNumber, + pageSize, + search + ); for await (const item of getOrganization['invitations']) { const getOrgRoles = await this.orgRoleService.getOrgRolesByIds(item['orgRoles']); (item['orgRoles'] as object) = getOrgRoles; - }; + } return getOrganization; } catch (error) { this.logger.error(`In create organization : ${JSON.stringify(error)}`); @@ -442,10 +614,25 @@ export class OrganizationService { * @returns organization roles */ - - async getOrgRoles(): Promise { + async getOrgRoles(orgId: string): Promise { try { - return this.orgRoleService.getOrgRoles(); + if (!orgId) { + throw new BadRequestException(ResponseMessages.organisation.error.orgIdIsRequired); + } + + const organizationDetails = await this.organizationRepository.getOrganizationDetails(orgId); + + if (!organizationDetails) { + throw new NotFoundException(ResponseMessages.organisation.error.orgNotFound); + } + + if (!organizationDetails.idpId) { + return this.orgRoleService.getOrgRoles(); + } + + const token = await this.clientRegistrationService.getManagementToken(); + + return this.clientRegistrationService.getAllClientRoles(organizationDetails.idpId, token); } catch (error) { this.logger.error(`In getOrgRoles : ${JSON.stringify(error)}`); throw new RpcException(error.response ? error.response : error); @@ -457,12 +644,8 @@ export class OrganizationService { * @param email * @returns */ - async checkInvitationExist( - email: string, - orgId: string - ): Promise { + async checkInvitationExist(email: string, orgId: string): Promise { try { - const query = { email, orgId @@ -493,19 +676,14 @@ export class OrganizationService { } } - /** - * - * @Body sendInvitationDto - * @returns createInvitation - */ - - - async createInvitation(bulkInvitationDto: BulkSendInvitationDto, userId: string, userEmail: string): Promise { + async createInvitationByOrgRoles( + bulkInvitationDto: BulkSendInvitationDto, + userEmail: string, + userId: string, + orgName: string + ): Promise { const { invitations, orgId } = bulkInvitationDto; - try { - const organizationDetails = await this.organizationRepository.getOrganizationDetails(orgId); - for (const invitation of invitations) { const { orgRoleId, email } = invitation; @@ -527,14 +705,111 @@ export class OrganizationService { await this.organizationRepository.createSendInvitation(email, String(orgId), String(userId), orgRoleId); try { - await this.sendInviteEmailTemplate(email, organizationDetails.name, orgRolesDetails, firstName, isUserExist); + await this.sendInviteEmailTemplate(email, orgName, orgRolesDetails, firstName, isUserExist); } catch (error) { throw new InternalServerErrorException(ResponseMessages.user.error.emailSend); } } + } + } + + async createInvitationByClientRoles( + bulkInvitationDto: BulkSendInvitationDto, + userEmail: string, + userId: string, + orgName: string, + idpId: string + ): Promise { + const { invitations, orgId } = bulkInvitationDto; + + const token = await this.clientRegistrationService.getManagementToken(); + const clientRolesList = await this.clientRegistrationService.getAllClientRoles(idpId, token); + const orgRoles = await this.orgRoleService.getOrgRoles(); + + for (const invitation of invitations) { + const { orgRoleId, email } = invitation; + + const isUserExist = await this.checkUserExistInPlatform(email); + + const userData = await this.getUserFirstName(userEmail); + + const { firstName } = userData; + + const matchedRoles = clientRolesList + .filter((role) => orgRoleId.includes(role.id.trim())) + .map((role) => role.name); + + if (orgRoleId.length !== matchedRoles.length) { + throw new NotFoundException(ResponseMessages.organisation.error.orgRoleIdNotFound); + } + + const filteredOrgRoles = orgRoles.filter((role) => matchedRoles.includes(role.name.trim())); + + const isInvitationExist = await this.checkInvitationExist(email, orgId); + + if (!isInvitationExist && userEmail !== invitation.email) { + + await this.organizationRepository.createSendInvitation( + email, + String(orgId), + String(userId), + filteredOrgRoles.map((role) => role.id) + ); + + try { + await this.sendInviteEmailTemplate( + email, + orgName, + filteredOrgRoles, + firstName, + isUserExist + ); + } catch (error) { + throw new InternalServerErrorException(ResponseMessages.user.error.emailSend); + } + } + } + } + + /** + * + * @Body sendInvitationDto + * @returns createInvitation + */ + + async createInvitation(bulkInvitationDto: BulkSendInvitationDto, userId: string, userEmail: string): Promise { + const { orgId } = bulkInvitationDto; + + try { + const organizationDetails = await this.organizationRepository.getOrganizationDetails(orgId); + + if (!organizationDetails) { + throw new NotFoundException(ResponseMessages.organisation.error.orgNotFound); + } + if (!organizationDetails.idpId) { + await this.createInvitationByOrgRoles( + bulkInvitationDto, + userEmail, + userId, + organizationDetails.name + ); + } else { + await this.createInvitationByClientRoles( + bulkInvitationDto, + userEmail, + userId, + organizationDetails.name, + organizationDetails.idpId + ); } - await this.userActivityService.createActivity(userId, organizationDetails.id, `Invitations sent for ${organizationDetails.name}`, 'Get started with user role management once invitations accepted'); + + await this.userActivityService.createActivity( + userId, + organizationDetails.id, + `Invitations sent for ${organizationDetails.name}`, + 'Get started with user role management once invitations accepted' + ); return ResponseMessages.organisation.success.createInvitation; } catch (error) { this.logger.error(`In send Invitation : ${JSON.stringify(error)}`); @@ -554,7 +829,7 @@ export class OrganizationService { email: string, orgName: string, orgRolesDetails: object[], - firstName:string, + firstName: string, isUserExist: boolean ): Promise { const platformConfigData = await this.prisma.platform_config.findMany(); @@ -563,9 +838,15 @@ export class OrganizationService { const emailData = new EmailDto(); emailData.emailFrom = platformConfigData[0].emailFrom; emailData.emailTo = email; - emailData.emailSubject = `Invitation to join “${orgName}” on CREDEBL`; + emailData.emailSubject = `Invitation to join “${orgName}” on ${process.env.PLATFORM_NAME}`; - emailData.emailHtml = await urlEmailTemplate.sendInviteEmailTemplate(email, orgName, orgRolesDetails, firstName, isUserExist); + emailData.emailHtml = await urlEmailTemplate.sendInviteEmailTemplate( + email, + orgName, + orgRolesDetails, + firstName, + isUserExist + ); //Email is sent to user for the verification through emailData const isEmailSent = await sendEmail(emailData); @@ -600,7 +881,7 @@ export class OrganizationService { const pattern = { cmd: 'get-user-by-mail' }; const payload = { email: userEmail }; - const userData = await this.organizationServiceProxy + const userData = await this.organizationServiceProxy .send(pattern, payload) .toPromise() .catch((error) => { @@ -612,12 +893,38 @@ export class OrganizationService { }, error.status ); - }); - return userData; - } - + }); + return userData; + } + + async getUserUserId(userId: string): Promise { + const pattern = { cmd: 'get-user-by-user-id' }; + // const payload = { id: userId }; + + const userData = await this.organizationServiceProxy + .send(pattern, userId) + .toPromise() + .catch((error) => { + this.logger.error(`catch: ${JSON.stringify(error)}`); + throw new HttpException( + { + status: error.status, + error: error.error, + message: error.message + }, + error.status + ); + }); + return userData; + } - async fetchUserInvitation(email: string, status: string, pageNumber: number, pageSize: number, search = ''): Promise { + async fetchUserInvitation( + email: string, + status: string, + pageNumber: number, + pageSize: number, + search = '' + ): Promise { try { return this.organizationRepository.getAllOrgInvitations(email, status, pageNumber, pageSize, search); } catch (error) { @@ -626,6 +933,49 @@ export class OrganizationService { } } + async updateClientInvitation( + // eslint-disable-next-line camelcase + invitation: org_invitations, + idpId: string, + userId: string, + keycloakUserId: string, + orgId: string, + status: string + ): Promise { + const token = await this.clientRegistrationService.getManagementToken(); + const clientRolesList = await this.clientRegistrationService.getAllClientRoles(idpId, token); + + const orgRoles = await this.orgRoleService.getOrgRolesByIds(invitation.orgRoles); + + const rolesPayload: { roleId: string; name: string; idpRoleId: string }[] = orgRoles.map((orgRole: IOrgRole) => { + let roleObj: { roleId: string; name: string; idpRoleId: string} = null; + + for (let index = 0; index < clientRolesList.length; index++) { + if (clientRolesList[index].name === orgRole.name) { + roleObj = { + roleId: orgRole.id, + name: orgRole.name, + idpRoleId: clientRolesList[index].id + }; + break; + } + } + + return roleObj; + }); + + const data = { + status + }; + + await Promise.all([ + this.organizationRepository.updateOrgInvitation(invitation.id, data), + this.clientRegistrationService.createUserClientRole(idpId, token, keycloakUserId, rolesPayload.map(role => ({id: role.idpRoleId, name: role.name}))), + this.userOrgRoleService.updateUserOrgRole(userId, orgId, rolesPayload) + ]); + + } + /** * * @param payload @@ -633,10 +983,10 @@ export class OrganizationService { */ async updateOrgInvitation(payload: UpdateInvitationDto): Promise { try { - const { orgId, status, invitationId, userId } = payload; + const { orgId, status, invitationId, userId, keycloakUserId, email } = payload; const invitation = await this.organizationRepository.getInvitationById(String(invitationId)); - if (!invitation) { + if (!invitation || (invitation && invitation.email !== email)) { throw new NotFoundException(ResponseMessages.user.error.invitationNotFound); } @@ -644,32 +994,112 @@ export class OrganizationService { throw new NotFoundException(ResponseMessages.user.error.invalidOrgId); } + const organizationDetails = await this.organizationRepository.getOrganizationDetails(orgId); + + if (!organizationDetails) { + throw new ConflictException(ResponseMessages.organisation.error.orgNotFound); + } + const invitationStatus = invitation.status as Invitation; if (!transition(invitationStatus, payload.status)) { - throw new BadRequestException(`${ResponseMessages.user.error.invitationStatusUpdateInvalid} ${invitation.status}`); + throw new BadRequestException( + `${ResponseMessages.user.error.invitationStatusUpdateInvalid} ${invitation.status}` + ); } const data = { status }; - await this.organizationRepository.updateOrgInvitation(invitationId, data); - if (status === Invitation.REJECTED) { + await this.organizationRepository.updateOrgInvitation(invitationId, data); return ResponseMessages.user.success.invitationReject; } - for (const roleId of invitation.orgRoles) { - await this.userOrgRoleService.createUserOrgRole(userId, roleId, orgId); + + if (organizationDetails.idpId) { + await this.updateClientInvitation(invitation, organizationDetails.idpId, userId, keycloakUserId, orgId, status); + } else { + await this.organizationRepository.updateOrgInvitation(invitationId, data); + + for (const roleId of invitation.orgRoles) { + await this.userOrgRoleService.createUserOrgRole(userId, roleId, orgId); + } } return ResponseMessages.user.success.invitationAccept; - } catch (error) { this.logger.error(`In updateOrgInvitation : ${error}`); throw new RpcException(error.response ? error.response : error); } } + async updateUserClientRoles( + // eslint-disable-next-line camelcase + roleIds: string[], + idpId: string, + userId: string, + orgId: string + ): Promise { + const token = await this.clientRegistrationService.getManagementToken(); + const clientRolesList = await this.clientRegistrationService.getAllClientRoles( + idpId, + token + ); + const orgRoles = await this.orgRoleService.getOrgRoles(); + + const matchedClientRoles = clientRolesList.filter((role) => roleIds.includes(role.id.trim())); + + if (roleIds.length !== matchedClientRoles.length) { + throw new NotFoundException(ResponseMessages.organisation.error.orgRoleIdNotFound); + } + + const rolesPayload: { roleId: string; name: string; idpRoleId: string }[] = matchedClientRoles.map( + (clientRole: IClientRoles) => { + let roleObj: { roleId: string; name: string; idpRoleId: string } = null; + + for (let index = 0; index < orgRoles.length; index++) { + if (orgRoles[index].name === clientRole.name) { + roleObj = { + roleId: orgRoles[index].id, + name: orgRoles[index].name, + idpRoleId: clientRole.id + }; + break; + } + } + + return roleObj; + } + ); + + const userData = await this.getUserUserId(userId); + + const [, deletedUserRoleRecords] = await Promise.all([ + this.clientRegistrationService.deleteUserClientRoles( + idpId, + token, + userData.keycloakUserId + ), + this.userOrgRoleService.deleteOrgRoles(userId, orgId) + ]); + + if (0 === deletedUserRoleRecords['count']) { + throw new InternalServerErrorException(ResponseMessages.organisation.error.updateUserRoles); + } + + const [, isUserRoleUpdated] = await Promise.all([ + this.clientRegistrationService.createUserClientRole( + idpId, + token, + userData.keycloakUserId, + rolesPayload.map((role) => ({ id: role.idpRoleId, name: role.name })) + ), + this.userOrgRoleService.updateUserOrgRole(userId, orgId, rolesPayload) + ]); + + return isUserRoleUpdated; + } + /** * * @param orgId @@ -679,26 +1109,45 @@ export class OrganizationService { */ async updateUserRoles(orgId: string, roleIds: string[], userId: string): Promise { try { - const isUserExistForOrg = await this.userOrgRoleService.checkUserOrgExist(userId, orgId); if (!isUserExistForOrg) { throw new NotFoundException(ResponseMessages.organisation.error.userNotFound); } - const isRolesExist = await this.orgRoleService.getOrgRolesByIds(roleIds); + const organizationDetails = await this.organizationRepository.getOrganizationDetails(orgId); - if (isRolesExist && 0 === isRolesExist.length) { - throw new NotFoundException(ResponseMessages.organisation.error.rolesNotExist); + if (!organizationDetails) { + throw new NotFoundException(ResponseMessages.organisation.error.orgNotFound); } - const deleteUserRecords = await this.userOrgRoleService.deleteOrgRoles(userId, orgId); + if (!organizationDetails.idpId) { + const isRolesExist = await this.orgRoleService.getOrgRolesByIds(roleIds); - if (0 === deleteUserRecords['count']) { - throw new InternalServerErrorException(ResponseMessages.organisation.error.updateUserRoles); - } + if (isRolesExist && 0 === isRolesExist.length) { + throw new NotFoundException(ResponseMessages.organisation.error.rolesNotExist); + } + + const deleteUserRecords = await this.userOrgRoleService.deleteOrgRoles(userId, orgId); + + if (0 === deleteUserRecords['count']) { + throw new InternalServerErrorException(ResponseMessages.organisation.error.updateUserRoles); + } - return this.userOrgRoleService.updateUserOrgRole(userId, orgId, roleIds); + for (const role of roleIds) { + this.userOrgRoleService.createUserOrgRole(userId, role, orgId); + } + + return true; + } else { + + return this.updateUserClientRoles( + roleIds, + organizationDetails.idpId, + userId, + organizationDetails.id + ); + } } catch (error) { this.logger.error(`Error in updateUserRoles: ${JSON.stringify(error)}`); @@ -708,7 +1157,7 @@ export class OrganizationService { async getOrgDashboard(orgId: string): Promise { try { - return this.organizationRepository.getOrgDashboard(orgId); + return this.organizationRepository.getOrgDashboard(orgId); } catch (error) { this.logger.error(`In create organization : ${JSON.stringify(error)}`); throw new RpcException(error.response ? error.response : error); @@ -744,7 +1193,6 @@ export class OrganizationService { } } - async getOrgOwner(orgId: string): Promise { try { const orgDetails = await this.organizationRepository.getOrganizationOwnerDetails(orgId, OrgRoles.OWNER); @@ -755,7 +1203,6 @@ export class OrganizationService { } } - async deleteOrganization(orgId: string): Promise { try { const getAgent = await this.organizationRepository.getAgentEndPoint(orgId); @@ -767,7 +1214,6 @@ export class OrganizationService { let url; if (getAgent.orgAgentTypeId === OrgAgentType.DEDICATED) { url = `${getAgent.agentEndPoint}${CommonConstants.URL_DELETE_WALLET}`; - } else if (getAgent.orgAgentTypeId === OrgAgentType.SHARED) { url = `${getAgent.agentEndPoint}${CommonConstants.URL_DELETE_SHARED_WALLET}`.replace('#', getAgent.tenantId); } @@ -779,14 +1225,12 @@ export class OrganizationService { const deleteWallet = await this._deleteWallet(payload); if (deleteWallet) { - const orgDelete = await this.organizationRepository.deleteOrg(orgId); if (false === orgDelete) { throw new NotFoundException(ResponseMessages.organisation.error.deleteOrg); } } - return true; } catch (error) { this.logger.error(`delete organization: ${JSON.stringify(error)}`); @@ -805,18 +1249,20 @@ export class OrganizationService { return this.organizationServiceProxy .send(pattern, payload) .pipe( - map((response) => ( - { + map((response) => ({ response })) - ).toPromise() - .catch(error => { + ) + .toPromise() + .catch((error) => { this.logger.error(`catch: ${JSON.stringify(error)}`); throw new HttpException( { status: error.statusCode, error: error.message - }, error.error); + }, + error.error + ); }); } catch (error) { this.logger.error(`[_deleteWallet] - error in delete wallet : ${JSON.stringify(error)}`); @@ -824,7 +1270,6 @@ export class OrganizationService { } } - async _getOrgAgentApiKey(orgId: string): Promise { const pattern = { cmd: 'get-org-agent-api-key' }; const payload = { orgId }; @@ -835,10 +1280,112 @@ export class OrganizationService { return message; } catch (error) { this.logger.error(`catch: ${JSON.stringify(error)}`); - throw new HttpException({ + throw new HttpException( + { status: error.status, error: error.message - }, error.status); + }, + error.status + ); + } + } + + async registerOrgsMapUsers(): Promise { + + try { + + const unregisteredOrgsList = await this.organizationRepository.getUnregisteredClientOrgs(); + + if (!unregisteredOrgsList || 0 === unregisteredOrgsList.length) { + throw new NotFoundException('Unregistered client organizations not found'); + } + + for (const org of unregisteredOrgsList) { + const userOrgRoles = 0 < org['userOrgRoles'].length && org['userOrgRoles']; + + const ownerUserList = 0 < org['userOrgRoles'].length + && userOrgRoles.filter(userOrgRole => userOrgRole.orgRole.name === OrgRoles.OWNER); + + const ownerUser = 0 < ownerUserList.length && ownerUserList[0].user; + + const orgObj = { + id: org.id, + idpId: org.idpId, + name: org.name, + ownerId: ownerUser.id, + ownerEmail: ownerUser.email, + ownerKeycloakId: ownerUser.keycloakUserId + }; + + if (orgObj.ownerKeycloakId) { + const orgCredentials = await this.registerToKeycloak( + orgObj.name, + orgObj.id, + orgObj.ownerKeycloakId, + orgObj.ownerId, + true + ); + + const { clientId, idpId, clientSecret } = orgCredentials; + + const updateOrgData = { + clientId, + clientSecret: this.maskString(clientSecret), + idpId + }; + + const updatedOrg = await this.organizationRepository.updateOrganizationById(updateOrgData, orgObj.id); + + this.logger.log(`updatedOrg::`, updatedOrg); + + const usersToRegisterList = userOrgRoles.filter(userOrgRole => null !== userOrgRole.user.keycloakUserId); + + const token = await this.clientRegistrationService.getManagementToken(); + const clientRolesList = await this.clientRegistrationService.getAllClientRoles(idpId, token); + + const deletedUserDetails: string[] = []; + for (const userRole of usersToRegisterList) { + const user = userRole.user; + + const matchedClientRoles = clientRolesList.filter((role) => userRole.orgRole.name === role.name) + .map(clientRole => ({roleId: userRole.orgRole.id, idpRoleId: clientRole.id, name: clientRole.name})); + + if (!deletedUserDetails.includes(user.id)) { + const [, deletedUserRoleRecords] = await Promise.all([ + this.clientRegistrationService.deleteUserClientRoles(idpId, token, user.keycloakUserId), + this.userOrgRoleService.deleteOrgRoles(user.id, orgObj.id) + ]); + + this.logger.log(`deletedUserRoleRecords::`, deletedUserRoleRecords); + + deletedUserDetails.push(user.id); + } + + + await Promise.all([ + this.clientRegistrationService.createUserClientRole( + idpId, + token, + user.keycloakUserId, + matchedClientRoles.map((role) => ({ id: role.idpRoleId, name: role.name })) + ), + this.userOrgRoleService.updateUserOrgRole( + user.id, + orgObj.id, + matchedClientRoles.map((role) => ({ roleId: role.roleId, idpRoleId: role.idpRoleId })) + ) + ]); + this.logger.log(`Organization client created and users mapped to roles`); + + } + } + } + + return ''; + } catch (error) { + this.logger.error(`Error in registerOrgsMapUsers: ${JSON.stringify(error)}`); + throw new RpcException(error.response ? error.response : error); + } } @@ -869,4 +1416,27 @@ export class OrganizationService { throw new RpcException(error.response ? error.response : error); } } -} \ No newline at end of file + + async storeOrgWebhookEndpoint(orgId: string, notificationWebhook: string): Promise { + const pattern = { cmd: 'register-org-webhook-endpoint-for-notification' }; + const payload = { + orgId, + notificationWebhook + }; + + try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const message = await this.organizationServiceProxy.send(pattern, payload).toPromise(); + return message; + } catch (error) { + this.logger.error(`catch: ${JSON.stringify(error)}`); + throw new HttpException( + { + status: error.status, + error: error.message + }, + error.status + ); + } + } +} diff --git a/apps/organization/templates/organization-invitation.template.ts b/apps/organization/templates/organization-invitation.template.ts index 193886dc8..e3fb38f58 100644 --- a/apps/organization/templates/organization-invitation.template.ts +++ b/apps/organization/templates/organization-invitation.template.ts @@ -12,13 +12,13 @@ export class OrganizationInviteTemplate { const message = isUserExist ? `Please accept the invitation using the following link:` - : `To get started, kindly register on CREDEBL platform using this link:`; + : `To get started, kindly register on ${process.env.PLATFORM_NAME} platform using this link:`; const secondMessage = isUserExist - ? `After successful login into CREDEBL click on "Accept Organization Invitation" link on your dashboard.` + ? `After successful login into ${process.env.PLATFORM_NAME} click on "Accept Organization Invitation" link on your dashboard.` : `After successful registration, you can log in to the platform and click on “Accept Organization Invitation” on your dashboard.`; - const Button = isUserExist ? 'Accept Organization Invitation' : 'Register on CREDEBL'; + const Button = isUserExist ? `Accept Organization Invitation` : `Register on ${process.env.PLATFORM_NAME}`; return ` @@ -32,7 +32,7 @@ export class OrganizationInviteTemplate {
- CREDEBL logo + ${process.env.PLATFORM_NAME} logo
- For any assistance or questions while accessing your account, please do not hesitate to contact the support team at support@blockster.global. Our team will ensure a seamless onboarding experience for you. + For any assistance or questions while accessing your account, please do not hesitate to contact the support team at ${process.env.PUBLIC_PLATFORM_SUPPORT_EMAIL}. Our team will ensure a seamless onboarding experience for you.

- © Blockster Labs Pvt. Ltd. + © ${process.env.POWERED_BY}

diff --git a/apps/organization/templates/organization-onboard.template.ts b/apps/organization/templates/organization-onboard.template.ts index 0750a742e..7f4c8b3be 100644 --- a/apps/organization/templates/organization-onboard.template.ts +++ b/apps/organization/templates/organization-onboard.template.ts @@ -25,7 +25,7 @@ export class OnBoardVerificationRequest {
Credebl Logo
- +
diff --git a/apps/user/interfaces/user.interface.ts b/apps/user/interfaces/user.interface.ts index 7eaa65d52..503370357 100644 --- a/apps/user/interfaces/user.interface.ts +++ b/apps/user/interfaces/user.interface.ts @@ -5,6 +5,7 @@ export interface IUsersProfile { firstName?: string; lastName?: string; supabaseUserId?: string; + keycloakUserId?: string; userOrgRoles?: IUserOrgRole[]; } diff --git a/apps/user/repositories/user.repository.ts b/apps/user/repositories/user.repository.ts index f49d1cc79..95337baab 100644 --- a/apps/user/repositories/user.repository.ts +++ b/apps/user/repositories/user.repository.ts @@ -223,6 +223,7 @@ export class UserRepository { clientId: true, clientSecret: true, supabaseUserId: true, + keycloakUserId: true, userOrgRoles: { include: { orgRole: true, @@ -271,6 +272,7 @@ export class UserRepository { profileImg: true, publicProfile: true, supabaseUserId: true, + keycloakUserId: true, isEmailVerified: true, userOrgRoles: { select:{ diff --git a/apps/user/src/user.controller.ts b/apps/user/src/user.controller.ts index 35574e26e..76f995980 100644 --- a/apps/user/src/user.controller.ts +++ b/apps/user/src/user.controller.ts @@ -44,6 +44,11 @@ export class UserController { return loginRes; } + @MessagePattern({ cmd: 'refresh-token-details' }) + async refreshTokenDetails(refreshToken: string): Promise { + return this.userService.refreshTokenDetails(refreshToken); + } + @MessagePattern({ cmd: 'user-reset-password' }) async resetPassword(payload: IUserResetPassword): Promise { return this.userService.resetPassword(payload); @@ -90,6 +95,12 @@ export class UserController { async findUserByEmail(payload: { email }): Promise { return this.userService.findUserByEmail(payload); } + + @MessagePattern({ cmd: 'get-user-by-user-id' }) + async findUserByUserId(id: string): Promise { + return this.userService.findUserByUserId(id); + } + /** * @param credentialId * @returns User credentials diff --git a/apps/user/src/user.service.ts b/apps/user/src/user.service.ts index 37e6de446..c4a3e589b 100644 --- a/apps/user/src/user.service.ts +++ b/apps/user/src/user.service.ts @@ -221,7 +221,7 @@ export class UserService { if (!checkUserDetails) { throw new NotFoundException(ResponseMessages.user.error.emailIsNotVerified); } - if (checkUserDetails.keycloakUserId) { + if (checkUserDetails.keycloakUserId || (!checkUserDetails.keycloakUserId && checkUserDetails.supabaseUserId)) { throw new ConflictException(ResponseMessages.user.error.exists); } if (false === checkUserDetails.isEmailVerified) { @@ -271,8 +271,21 @@ export class UserService { keycloakDetails.keycloakUserId.toString() ); - const holderRoleData = await this.orgRoleService.getRole(OrgRoles.HOLDER); - await this.userOrgRoleService.createUserOrgRole(userDetails.id, holderRoleData.id); + const realmRoles = await this.clientRegistrationService.getAllRealmRoles(token); + + const holderRole = realmRoles.filter(role => role.name === OrgRoles.HOLDER); + const holderRoleData = 0 < holderRole.length && holderRole[0]; + + const payload = [ + { + id: holderRoleData.id, + name: holderRoleData.name + } + ]; + + await this.clientRegistrationService.createUserHolderRole(token, keycloakDetails.keycloakUserId.toString(), payload); + const holderOrgRole = await this.orgRoleService.getRole(OrgRoles.HOLDER); + await this.userOrgRoleService.createUserOrgRole(userDetails.id, holderOrgRole.id, null, holderRoleData.id); return ResponseMessages.user.success.signUpUser; } catch (error) { @@ -361,6 +374,23 @@ export class UserService { } } + async refreshTokenDetails(refreshToken: string): Promise { + + try { + try { + const tokenResponse = await this.clientRegistrationService.getAccessToken(refreshToken); + return tokenResponse; + } catch (error) { + throw new BadRequestException(ResponseMessages.user.error.invalidRefreshToken); + } + + } catch (error) { + this.logger.error(`In refreshTokenDetails : ${JSON.stringify(error)}`); + throw new RpcException(error.response ? error.response : error); + + } + } + async updateFidoVerifiedUser(email: string, isFidoVerified: boolean, password: string): Promise { if (isFidoVerified) { await this.userRepository.addUserPassword(email.toLowerCase(), password); @@ -470,11 +500,21 @@ export class UserService { } const decryptedPassword = await this.commonService.decryptPassword(password); - try { + try { + const authToken = await this.clientRegistrationService.getManagementToken(); userData.password = decryptedPassword; - await this.clientRegistrationService.resetPasswordOfUser(userData, process.env.KEYCLOAK_REALM, authToken); + if (userData.keycloakUserId) { + await this.clientRegistrationService.resetPasswordOfUser(userData, process.env.KEYCLOAK_REALM, authToken); + } else { + const keycloakDetails = await this.clientRegistrationService.createUser(userData, process.env.KEYCLOAK_REALM, authToken); + await this.userRepository.updateUserDetails(userData.id, + keycloakDetails.keycloakUserId.toString() + ); + } + await this.updateFidoVerifiedUser(email.toLowerCase(), userData.isFidoVerified, password); + } catch (error) { this.logger.error(`Error reseting the password`, error); throw new InternalServerErrorException('Error while reseting user password'); @@ -493,6 +533,11 @@ export class UserService { } } + findUserByUserId(id: string): Promise { + return this.userRepository.getUserById(id); + + } + async resetPassword(resetPasswordDto: IUserResetPassword): Promise { const { email, oldPassword, newPassword } = resetPasswordDto; @@ -562,7 +607,7 @@ export class UserService { tokenResponse.isRegisteredToSupabase = false; return tokenResponse; } catch (error) { - throw new UnauthorizedException(error?.message); + throw new UnauthorizedException(ResponseMessages.user.error.invalidCredentials); } } else { @@ -778,7 +823,7 @@ export class UserService { async acceptRejectInvitations(acceptRejectInvitation: AcceptRejectInvitationDto, userId: string): Promise { try { const userData = await this.userRepository.getUserById(userId); - return this.fetchInvitationsStatus(acceptRejectInvitation, userId, userData.email); + return this.fetchInvitationsStatus(acceptRejectInvitation, userData.keycloakUserId, userData.email, userId); } catch (error) { this.logger.error(`acceptRejectInvitations: ${error}`); throw new RpcException(error.response ? error.response : error); @@ -883,15 +928,16 @@ export class UserService { */ async fetchInvitationsStatus( acceptRejectInvitation: AcceptRejectInvitationDto, - userId: string, - email: string + keycloakUserId: string, + email: string, + userId: string ): Promise { try { const pattern = { cmd: 'update-invitation-status' }; const { orgId, invitationId, status } = acceptRejectInvitation; - const payload = { userId, orgId, invitationId, status, email }; + const payload = { userId, keycloakUserId, orgId, invitationId, status, email }; const invitationsData = await this.userServiceProxy .send(pattern, payload) @@ -974,6 +1020,8 @@ export class UserService { throw new ConflictException(ResponseMessages.user.error.verificationAlreadySent); } else if (userDetails && userDetails.keycloakUserId) { throw new ConflictException(ResponseMessages.user.error.exists); + } else if (userDetails && !userDetails.keycloakUserId && userDetails.supabaseUserId) { + throw new ConflictException(ResponseMessages.user.error.exists); } else if (null === userDetails) { return { isRegistrationCompleted: false, diff --git a/apps/user/templates/arbiter-template.ts b/apps/user/templates/arbiter-template.ts index efeee0331..962c39710 100644 --- a/apps/user/templates/arbiter-template.ts +++ b/apps/user/templates/arbiter-template.ts @@ -86,7 +86,7 @@ export class ArbiterTemplate {

Date: 24, 25, 26 November 2023 | Place: Cidco Exhibition Centre, Navi Mumbai, India
-
Blockchain-based certificate issued using credebl.id, by Blockster Labs Pvt. Ltd.
+
Blockchain-based certificate issued using ${process.env.PLATFORM_WEB_URL}, by ${process.env.POWERED_BY}
diff --git a/apps/user/templates/participant-template.ts b/apps/user/templates/participant-template.ts index f2738fb23..c9250dd84 100644 --- a/apps/user/templates/participant-template.ts +++ b/apps/user/templates/participant-template.ts @@ -82,7 +82,7 @@ export class ParticipantTemplate {

exceptional memory skills demonstrated during the competition.

Date: 24, 25, 26 November 2023 | Place: Cidco Exhibition Centre, Navi Mumbai, India
-
Blockchain-based certificate issued using credebl.id, by Blockster Labs Pvt. Ltd.
+
Blockchain-based certificate issued using ${process.env.PLATFORM_WEB_URL}, by ${process.env.POWERED_BY}
diff --git a/apps/user/templates/reset-password-template.ts b/apps/user/templates/reset-password-template.ts index eb0019e68..ec05ecdde 100644 --- a/apps/user/templates/reset-password-template.ts +++ b/apps/user/templates/reset-password-template.ts @@ -23,7 +23,7 @@ export class URLUserResetPasswordTemplate {
- CREDEBL logo + ${process.env.PLATFORM_NAME} logo
@@ -46,11 +46,11 @@ export class URLUserResetPasswordTemplate {
- For any assistance or questions while accessing your account, please do not hesitate to contact the support team at support@blockster.global. Our team will ensure a seamless onboarding experience for you. + For any assistance or questions while accessing your account, please do not hesitate to contact the support team at ${process.env.PUBLIC_PLATFORM_SUPPORT_EMAIL}. Our team will ensure a seamless onboarding experience for you.

- © Blockster Labs Pvt. Ltd. + © ${process.env.POWERED_BY}

diff --git a/apps/user/templates/user-email-template.ts b/apps/user/templates/user-email-template.ts index 857a1bb7a..dd648c539 100644 --- a/apps/user/templates/user-email-template.ts +++ b/apps/user/templates/user-email-template.ts @@ -23,7 +23,7 @@ export class URLUserEmailTemplate {
- CREDEBL logo + ${process.env.PLATFORM_NAME} logo
@@ -48,11 +48,11 @@ export class URLUserEmailTemplate {
- For any assistance or questions while accessing your account, please do not hesitate to contact the support team at support@blockster.global. Our team will ensure a seamless onboarding experience for you. + For any assistance or questions while accessing your account, please do not hesitate to contact the support team at ${process.env.PUBLIC_PLATFORM_SUPPORT_EMAIL}. Our team will ensure a seamless onboarding experience for you.

- © Blockster Labs Pvt. Ltd. + © ${process.env.POWERED_BY}

diff --git a/apps/user/templates/user-onboard.template.ts b/apps/user/templates/user-onboard.template.ts index 66a60b702..99faeb07d 100644 --- a/apps/user/templates/user-onboard.template.ts +++ b/apps/user/templates/user-onboard.template.ts @@ -25,7 +25,7 @@ export class OnBoardVerificationRequest {
Credebl Logoexceptional memory skills demonstrated during the competition.

Date: 24, 25, 26 November 2023 | Place: Cidco Exhibition Centre, Navi Mumbai, India
-
Blockchain-based certificate issued using credebl.id, by Blockster Labs Pvt. Ltd.
+
Blockchain-based certificate issued using ${process.env.PLATFORM_WEB_URL}, by ${process.env.POWERED_BY}
diff --git a/apps/user/templates/world-record-template.ts b/apps/user/templates/world-record-template.ts index 3eb30cf8c..27c9fbfda 100644 --- a/apps/user/templates/world-record-template.ts +++ b/apps/user/templates/world-record-template.ts @@ -84,7 +84,7 @@ export class WorldRecordTemplate {

exceptional memory skills demonstrated during the competition.

Date: 24, 25, 26 November 2023 | Place: Cidco Exhibition Centre, Navi Mumbai, India
-
Blockchain-based certificate issued using credebl.id, by Blockster Labs Pvt. Ltd.
+
Blockchain-based certificate issued using ${process.env.PLATFORM_WEB_URL}, by ${process.env.POWERED_BY}
diff --git a/apps/utility/interfaces/shortening-url.interface.ts b/apps/utility/interfaces/shortening-url.interface.ts index 4a18a4078..7772e04ad 100644 --- a/apps/utility/interfaces/shortening-url.interface.ts +++ b/apps/utility/interfaces/shortening-url.interface.ts @@ -1,10 +1,10 @@ export interface IShorteningUrlData { - referenceId: string, - schemaId: string, - credDefId: string, - invitationUrl: string, - attributes: IAttributes[] + referenceId: string; + schemaId: string; + credDefId: string; + invitationUrl: string; + attributes: IAttributes[]; } export interface IAttributes { - [key: string]: string - } + [key: string]: string; +} diff --git a/apps/utility/src/utilities.controller.ts b/apps/utility/src/utilities.controller.ts index cf08cbe18..f9199625d 100644 --- a/apps/utility/src/utilities.controller.ts +++ b/apps/utility/src/utilities.controller.ts @@ -1,11 +1,14 @@ -import { Controller } from '@nestjs/common'; +import { Controller, Logger } from '@nestjs/common'; import { MessagePattern } from '@nestjs/microservices'; import { UtilitiesService } from './utilities.service'; import { IShorteningUrlData } from '../interfaces/shortening-url.interface'; @Controller() export class UtilitiesController { - constructor(private readonly utilitiesService: UtilitiesService) {} + constructor( + private readonly utilitiesService: UtilitiesService, + private readonly logger: Logger + ) {} @MessagePattern({ cmd: 'create-shortening-url' }) async createAndStoreShorteningUrl(payload: IShorteningUrlData): Promise { @@ -16,4 +19,15 @@ export class UtilitiesController { async getShorteningUrl(referenceId: string): Promise { return this.utilitiesService.getShorteningUrl(referenceId); } -} \ No newline at end of file + + @MessagePattern({ cmd: 'store-object-return-url' }) + async storeObject(payload: { persistent: boolean; storeObj: unknown }): Promise { + try { + const url: string = await this.utilitiesService.storeObject(payload); + return url; + } catch (error) { + this.logger.error(error); + throw new Error('Error occured in Utility Microservices Controller'); + } + } +} diff --git a/apps/utility/src/utilities.module.ts b/apps/utility/src/utilities.module.ts index 5045ac127..5048add05 100644 --- a/apps/utility/src/utilities.module.ts +++ b/apps/utility/src/utilities.module.ts @@ -7,6 +7,7 @@ import { PrismaService } from '@credebl/prisma-service'; import { UtilitiesController } from './utilities.controller'; import { UtilitiesService } from './utilities.service'; import { UtilitiesRepository } from './utilities.repository'; +import { AwsService } from '@credebl/aws'; @Module({ imports: [ @@ -21,6 +22,6 @@ import { UtilitiesRepository } from './utilities.repository'; CacheModule.register() ], controllers: [UtilitiesController], - providers: [UtilitiesService, Logger, PrismaService, UtilitiesRepository] + providers: [UtilitiesService, Logger, PrismaService, UtilitiesRepository, AwsService] }) export class UtilitiesModule { } diff --git a/apps/utility/src/utilities.service.ts b/apps/utility/src/utilities.service.ts index fbd300063..a1c66bdd8 100644 --- a/apps/utility/src/utilities.service.ts +++ b/apps/utility/src/utilities.service.ts @@ -1,12 +1,16 @@ import { Injectable, Logger } from '@nestjs/common'; import { RpcException } from '@nestjs/microservices'; import { UtilitiesRepository } from './utilities.repository'; +import { AwsService } from '@credebl/aws'; +import { S3 } from 'aws-sdk'; +import { v4 as uuidv4 } from 'uuid'; @Injectable() export class UtilitiesService { constructor( private readonly logger: Logger, - private readonly utilitiesRepository: UtilitiesRepository + private readonly utilitiesRepository: UtilitiesRepository, + private readonly awsService: AwsService ) { } async createAndStoreShorteningUrl(payload): Promise { @@ -44,4 +48,16 @@ export class UtilitiesService { throw new RpcException(error); } } + + async storeObject(payload: {persistent: boolean, storeObj: unknown}): Promise { + try { + const uuid = uuidv4(); + const uploadResult:S3.ManagedUpload.SendData = await this.awsService.storeObject(payload.persistent, uuid, payload.storeObj); + const url: string = `https://${uploadResult.Bucket}.s3.${process.env.AWS_S3_STOREOBJECT_REGION}.amazonaws.com/${uploadResult.Key}`; + return url; + } catch (error) { + this.logger.error(error); + throw new Error('An error occurred while uploading data to S3. Error::::::'); + } + } } diff --git a/apps/verification/src/interfaces/verification.interface.ts b/apps/verification/src/interfaces/verification.interface.ts index d35a2daaf..b9172233e 100644 --- a/apps/verification/src/interfaces/verification.interface.ts +++ b/apps/verification/src/interfaces/verification.interface.ts @@ -1,8 +1,9 @@ -import { AutoAccept } from "@credebl/enum/enum"; -import { IUserRequest } from "@credebl/user-request/user-request.interface"; +import { AutoAccept } from '@credebl/enum/enum'; +import { IUserRequest } from '@credebl/user-request/user-request.interface'; interface IProofRequestAttribute { - attributeName: string; + attributeName?: string; + attributeNames?:string[]; condition?: string; value?: string; credDefId?: string; @@ -30,22 +31,25 @@ export interface IGetAllProofPresentations { export interface IGetProofPresentationById { url: string; - apiKey: string; + apiKey?: string; + orgId?: string; } export interface IVerifyPresentation { url: string; - apiKey: string; + apiKey?: string; + orgId?: string; } export interface IVerifiedProofData { url: string; - apiKey: string; + apiKey?: string; + orgId?: string } export interface IProofPresentationData { proofId: string; - orgId: string; + orgId: string; user: IUserRequest; } @@ -65,7 +69,8 @@ interface IRequestedAttributes { } interface IRequestedAttributesName { - name: string; + name?: string; + names?: string; restrictions: IRequestedRestriction[] } @@ -81,24 +86,97 @@ interface IRequestedPredicatesName { interface IRequestedRestriction { cred_def_id?: string; schema_id?: string; + schema_issuer_did?: string; + schema_name?: string; + issuer_did?: string; + schema_version?: string; +} +export interface ISchema { + uri:string; } +export interface IFields { + path: string[]; + } +export interface IConstraints { + fields: IFields[]; + } -export interface ISendProofRequestPayload { +export interface IInputDescriptors { + + id:string; + name?:string; + purpose?:string; + schema:ISchema[]; + constraints?:IConstraints; + +} + +export interface IProofRequestPresentationDefinition { + id:string; + input_descriptors:IInputDescriptors[]; +} + +export interface IPresentationExchange { + presentationDefinition:IProofRequestPresentationDefinition; + +} +export interface IPresentationExchangeProofFormats { + presentationExchange : IPresentationExchange; +} +export interface ISendPresentationExchangeProofRequestPayload { protocolVersion: string; comment: string; - connectionId?: string; - proofFormats: IProofFormats; + proofFormats: IPresentationExchangeProofFormats; autoAcceptProof: string; label?: string; +} +export interface IPresentationExchangeProofRequestPayload { + url: string; + apiKey?: string; + proofRequestPayload: ISendPresentationExchangeProofRequestPayload; + orgId?: string; +} + +export interface ISendProofRequestPayload { + protocolVersion?: string; + comment?: string; + connectionId?: string; + proofFormats?: IProofFormats; + autoAcceptProof?: string; + label?: string; + goalCode?: string; + parentThreadId?: string; + willConfirm?: boolean; + imageUrl?: string; + emailId?: string[] + isShortenUrl?: boolean; + type?:string; + presentationDefinition?:IProofRequestPresentationDefinition; + reuseConnection?: boolean; + recipientKey?:string; +} + +export interface IWSendProofRequestPayload { + protocolVersion?: string; + comment?: string; + connectionId?: string; + proofFormats?: IProofFormats; + autoAcceptProof?: string; + label?: string; goalCode?: string; parentThreadId?: string; willConfirm?: boolean; + imageUrl?: string; + emailId?: string[]; + type?:string; + presentationDefinition?:IProofRequestPresentationDefinition; } export interface IProofRequestPayload { url: string; - apiKey: string; - proofRequestPayload: ISendProofRequestPayload; + apiKey?: string; + orgId?: string + proofRequestPayload: ISendProofRequestPayload | ISendPresentationExchangeProofRequestPayload; } interface IWebhookPresentationProof { @@ -132,7 +210,7 @@ export interface IProofRequests { proofRequestsSearchCriteria: IProofRequestSearchCriteria; user: IUserRequest; orgId: string; - } +} export interface IProofRequestSearchCriteria { pageNumber: number; @@ -140,5 +218,8 @@ export interface IProofRequestSearchCriteria { sortField: string; sortBy: string; searchByText: string; - } - +} + +export interface IInvitation{ + invitationUrl?: string; +} \ No newline at end of file diff --git a/apps/verification/src/repositories/verification.repository.ts b/apps/verification/src/repositories/verification.repository.ts index 31b32be28..b64b24145 100644 --- a/apps/verification/src/repositories/verification.repository.ts +++ b/apps/verification/src/repositories/verification.repository.ts @@ -2,7 +2,7 @@ import { ResponseMessages } from '@credebl/common/response-messages'; import { PrismaService } from '@credebl/prisma-service'; import { Injectable, Logger, NotFoundException } from '@nestjs/common'; // eslint-disable-next-line camelcase -import { org_agents, organisation, platform_config, presentations } from '@prisma/client'; +import { agent_invitations, org_agents, organisation, platform_config, presentations } from '@prisma/client'; import { IProofPresentation } from '../interfaces/verification.interface'; import { IProofRequestSearchCriteria } from '../interfaces/verification.interface'; import { IUserRequest } from '@credebl/user-request/user-request.interface'; @@ -185,4 +185,21 @@ export class VerificationRepository { throw error; } } + + // eslint-disable-next-line camelcase + async getRecipientKeyByOrgId(orgId: string): Promise { + try { + return this.prisma.agent_invitations.findMany({ + where: { + orgId + }, + orderBy: { + createDateTime: 'asc' // or 'desc' for descending order + } + }); + } catch (error) { + this.logger.error(`Error in getRecipientKey in verification repository: ${error.message}`); + throw error; + } + } } diff --git a/apps/verification/src/verification.controller.ts b/apps/verification/src/verification.controller.ts index 2ca29829d..34e8bd9db 100644 --- a/apps/verification/src/verification.controller.ts +++ b/apps/verification/src/verification.controller.ts @@ -1,7 +1,7 @@ import { Controller } from '@nestjs/common'; import { VerificationService } from './verification.service'; import { MessagePattern } from '@nestjs/microservices'; -import { IProofPresentation, IProofPresentationData, IProofRequests, IRequestProof } from './interfaces/verification.interface'; +import { IProofPresentation, IProofPresentationData, IProofRequests, IRequestProof, ISendProofRequestPayload } from './interfaces/verification.interface'; import { IUserRequest } from '@credebl/user-request/user-request.interface'; import { presentations } from '@prisma/client'; import { IProofPresentationDetails, IProofPresentationList } from '@credebl/common/interfaces/verification.interface'; @@ -63,8 +63,8 @@ export class VerificationController { } @MessagePattern({ cmd: 'send-out-of-band-proof-request' }) - async sendOutOfBandPresentationRequest(payload: { outOfBandRequestProof: IRequestProof, user: IUserRequest }): Promise { - return this.verificationService.sendOutOfBandPresentationRequest(payload.outOfBandRequestProof); + async sendOutOfBandPresentationRequest(payload: { outOfBandRequestProof: ISendProofRequestPayload, user: IUserRequest }): Promise { + return this.verificationService.sendOutOfBandPresentationRequest(payload.outOfBandRequestProof, payload.user); } @MessagePattern({ cmd: 'get-verified-proof-details' }) diff --git a/apps/verification/src/verification.service.ts b/apps/verification/src/verification.service.ts index a63fcf1b4..61478d8c4 100644 --- a/apps/verification/src/verification.service.ts +++ b/apps/verification/src/verification.service.ts @@ -2,10 +2,10 @@ import { BadRequestException, HttpException, Inject, Injectable, InternalServerErrorException, Logger, NotFoundException } from '@nestjs/common'; import { ClientProxy, RpcException } from '@nestjs/microservices'; import { map } from 'rxjs/operators'; -import { IGetAllProofPresentations, IProofRequestSearchCriteria, IGetProofPresentationById, IProofPresentation, IProofRequestPayload, IRequestProof, ISendProofRequestPayload, IVerifyPresentation, IVerifiedProofData } from './interfaces/verification.interface'; +import { IGetAllProofPresentations, IProofRequestSearchCriteria, IGetProofPresentationById, IProofPresentation, IProofRequestPayload, IRequestProof, ISendProofRequestPayload, IVerifyPresentation, IVerifiedProofData, IInvitation} from './interfaces/verification.interface'; import { VerificationRepository } from './repositories/verification.repository'; import { CommonConstants } from '@credebl/common/common.constant'; -import { org_agents, organisation, presentations } from '@prisma/client'; +import { agent_invitations, org_agents, organisation, presentations } from '@prisma/client'; import { OrgAgentType } from '@credebl/enum/enum'; import { ResponseMessages } from '@credebl/common/response-messages'; import * as QRCode from 'qrcode'; @@ -16,6 +16,7 @@ import { Cache } from 'cache-manager'; import { CACHE_MANAGER } from '@nestjs/cache-manager'; import { IUserRequest } from '@credebl/user-request/user-request.interface'; import { IProofPresentationDetails, IProofPresentationList } from '@credebl/common/interfaces/verification.interface'; +import { ProofRequestType } from 'apps/api-gateway/src/verification/enum/verification.enum'; @Injectable() export class VerificationService { @@ -81,15 +82,15 @@ export class VerificationService { data: getProofRequestsList.proofRequestsList }; - return proofPresentationsResponse; - } catch (error) { + return proofPresentationsResponse; + } catch (error) { - this.logger.error( - `[getProofRequests] [NATS call]- error in fetch proof requests details : ${JSON.stringify(error)}` - ); - throw new RpcException(error.response ? error.response : error); - } -} + this.logger.error( + `[getProofRequests] [NATS call]- error in fetch proof requests details : ${JSON.stringify(error)}` + ); + throw new RpcException(error.response ? error.response : error); + } + } /** * Consume agent API for get all proof presentations @@ -120,12 +121,9 @@ export class VerificationService { const verificationMethodLabel = 'get-proof-presentation-by-id'; const orgAgentType = await this.verificationRepository.getOrgAgentType(getAgentDetails?.orgAgentTypeId); - let apiKey: string = await this.cacheService.get(CommonConstants.CACHE_APIKEY_KEY); const url = await this.getAgentUrl(verificationMethodLabel, orgAgentType, getAgentDetails?.agentEndPoint, getAgentDetails?.tenantId, '', proofId); - if (!apiKey || null === apiKey || undefined === apiKey) { - apiKey = await this._getOrgAgentApiKey(orgId); - } - const payload = { apiKey, url }; + + const payload = { orgId, url }; const getProofPresentationById = await this._getProofPresentationById(payload); return getProofPresentationById?.response; @@ -139,9 +137,9 @@ export class VerificationService { statusCode: error?.response?.status, error: errorStack }); - } else { - throw new RpcException(error.response ? error.response : error); - } + } else { + throw new RpcException(error.response ? error.response : error); + } } } @@ -221,19 +219,15 @@ export class VerificationService { const orgAgentType = await this.verificationRepository.getOrgAgentType(getAgentDetails?.orgAgentTypeId); const verificationMethodLabel = 'request-proof'; const url = await this.getAgentUrl(verificationMethodLabel, orgAgentType, getAgentDetails?.agentEndPoint, getAgentDetails?.tenantId); - let apiKey: string = await this.cacheService.get(CommonConstants.CACHE_APIKEY_KEY); - this.logger.log(`cachedApiKey----${apiKey}`); - if (!apiKey || null === apiKey || undefined === apiKey) { - apiKey = await this._getOrgAgentApiKey(requestProof.orgId); - } - const payload = { apiKey, url, proofRequestPayload }; + + const payload = { orgId: requestProof.orgId, url, proofRequestPayload }; const getProofPresentationById = await this._sendProofRequest(payload); return getProofPresentationById?.response; } catch (error) { this.logger.error(`[verifyPresentation] - error in verify presentation : ${JSON.stringify(error)}`); this.verificationErrorHandling(error); - + } } @@ -258,25 +252,22 @@ export class VerificationService { } } - /** - * Verify proof presentation - * @param proofId - * @param orgId - * @returns Verified proof presentation details - */ - async verifyPresentation(proofId: string, orgId: string): Promise { + /** + * Verify proof presentation + * @param proofId + * @param orgId + * @returns Verified proof presentation details + */ + async verifyPresentation(proofId: string, orgId: string): Promise { try { const getAgentData = await this.verificationRepository.getAgentEndPoint(orgId); const orgAgentTypeData = await this.verificationRepository.getOrgAgentType(getAgentData?.orgAgentTypeId); - let apiKey: string = await this.cacheService.get(CommonConstants.CACHE_APIKEY_KEY); - + const verificationMethod = 'accept-presentation'; const url = await this.getAgentUrl(verificationMethod, orgAgentTypeData, getAgentData?.agentEndPoint, getAgentData?.tenantId, '', proofId); - if (!apiKey || null === apiKey || undefined === apiKey) { - apiKey = await this._getOrgAgentApiKey(orgId); - } - const payload = { apiKey, url }; + + const payload = { orgId, url }; const getProofPresentationById = await this._verifyPresentation(payload); return getProofPresentationById?.response; } catch (error) { @@ -290,8 +281,8 @@ export class VerificationService { error: errorStack }); } else { - throw new RpcException(error.response ? error.response : error); - } + throw new RpcException(error.response ? error.response : error); + } } } @@ -333,60 +324,94 @@ export class VerificationService { * @param outOfBandRequestProof * @returns Get requested proof presentation details */ - async sendOutOfBandPresentationRequest(outOfBandRequestProof: IRequestProof): Promise { + async sendOutOfBandPresentationRequest(outOfBandRequestProof: ISendProofRequestPayload, user: IUserRequest): Promise { try { - this.logger.log(`-------outOfBandRequestProof------${JSON.stringify(outOfBandRequestProof)}`); - const comment = outOfBandRequestProof.comment || ''; - const protocolVersion = outOfBandRequestProof.protocolVersion || 'v1'; - const autoAcceptProof = outOfBandRequestProof.autoAcceptProof || 'never'; - - const { requestedAttributes, requestedPredicates } = await this._proofRequestPayload(outOfBandRequestProof); + // const { requestedAttributes, requestedPredicates } = await this._proofRequestPayload(outOfBandRequestProof); + + const [getAgentDetails, getOrganization] = await Promise.all([ + this.verificationRepository.getAgentEndPoint(user.orgId), + this.verificationRepository.getOrganization(user.orgId) + ]); + const label = getOrganization?.name; - const [getAgentDetails, organizationDetails] = await Promise.all([ - this.verificationRepository.getAgentEndPoint(outOfBandRequestProof.orgId), - this.verificationRepository.getOrganization(outOfBandRequestProof.orgId) - ]); + if (getOrganization?.logoUrl) { + outOfBandRequestProof['imageUrl'] = getOrganization?.logoUrl; + } + + outOfBandRequestProof['label'] = label; const orgAgentType = await this.verificationRepository.getOrgAgentType(getAgentDetails?.orgAgentTypeId); - let apiKey: string = await this.cacheService.get(CommonConstants.CACHE_APIKEY_KEY); const verificationMethodLabel = 'create-request-out-of-band'; const url = await this.getAgentUrl(verificationMethodLabel, orgAgentType, getAgentDetails?.agentEndPoint, getAgentDetails?.tenantId); - this.logger.log(`cachedApiKey----${apiKey}`); - if (!apiKey || null === apiKey || undefined === apiKey) { - apiKey = await this._getOrgAgentApiKey(outOfBandRequestProof.orgId); + + + // Destructuring 'outOfBandRequestProof' to remove emailId, as it is not used while agent operation + const { isShortenUrl, emailId, type, reuseConnection, ...updateOutOfBandRequestProof } = outOfBandRequestProof; + let recipientKey: string | undefined; + if (true === reuseConnection) { + const data: agent_invitations[] = await this.verificationRepository.getRecipientKeyByOrgId(user.orgId); + if (data && 0 < data.length) { + const [firstElement] = data; + recipientKey = firstElement?.recipientKey ?? undefined; } - const payload: IProofRequestPayload - = { - apiKey, + } + outOfBandRequestProof.autoAcceptProof = outOfBandRequestProof.autoAcceptProof || 'always'; + + + let payload: IProofRequestPayload; + + if (ProofRequestType.INDY === type) { + updateOutOfBandRequestProof.protocolVersion = updateOutOfBandRequestProof.protocolVersion || 'v1'; + updateOutOfBandRequestProof.recipientKey = recipientKey || undefined; + payload = { + orgId: user.orgId, url, - proofRequestPayload: { - protocolVersion, - comment, - label: organizationDetails?.name, - proofFormats: { - indy: { - name: 'Proof Request', - version: '1.0', - requested_attributes: requestedAttributes, - requested_predicates: requestedPredicates - } - }, - autoAcceptProof, - goalCode: outOfBandRequestProof.goalCode || undefined, - parentThreadId: outOfBandRequestProof.parentThreadId || undefined, - willConfirm: outOfBandRequestProof.willConfirm || undefined - } + proofRequestPayload: updateOutOfBandRequestProof }; + } + + if (ProofRequestType.PRESENTATIONEXCHANGE === type) { + + payload = { + orgId: user.orgId, + url, + proofRequestPayload: { + protocolVersion:outOfBandRequestProof.protocolVersion || 'v2', + comment:outOfBandRequestProof.comment, + label, + proofFormats: { + presentationExchange: { + presentationDefinition: { + id: outOfBandRequestProof.presentationDefinition.id, + input_descriptors: [...outOfBandRequestProof.presentationDefinition.input_descriptors] + } + } + }, + autoAcceptProof:outOfBandRequestProof.autoAcceptProof, + recipientKey:recipientKey || undefined + } + }; + } - if (outOfBandRequestProof.emailId) { - const batchSize = 100; // Define the batch size according to your needs - const { emailId } = outOfBandRequestProof; // Assuming it's an array - await this.sendEmailInBatches(payload, emailId, getAgentDetails, organizationDetails, batchSize); - return true; + if (emailId) { + await this.sendEmailInBatches(payload, emailId, getAgentDetails, getOrganization); + return true; } else { - return this.generateOOBProofReq(payload, getAgentDetails); + const presentationProof: IInvitation = await this.generateOOBProofReq(payload); + const proofRequestInvitationUrl: string = presentationProof.invitationUrl; + if (isShortenUrl) { + const shortenedUrl: string = await this.storeVerificationObjectAndReturnUrl(proofRequestInvitationUrl, false); + this.logger.log('shortenedUrl', shortenedUrl); + if (shortenedUrl) { + presentationProof.invitationUrl = shortenedUrl; + } + } + if (!presentationProof) { + throw new Error(ResponseMessages.verification.error.proofPresentationNotFound); + } + return presentationProof; } } catch (error) { this.logger.error(`[sendOutOfBandPresentationRequest] - error in out of band proof request : ${error.message}`); @@ -394,18 +419,18 @@ export class VerificationService { } } + async storeVerificationObjectAndReturnUrl(storeObj: string, persistent: boolean): Promise { + //nats call in agent-service to create an invitation url + const pattern = { cmd: 'store-object-return-url' }; + const payload = { persistent, storeObj }; + const message = await this.natsCall(pattern, payload); + return message.response; + } + - private async generateOOBProofReq(payload: IProofRequestPayload, getAgentDetails: org_agents): Promise { - let agentApiKey: string = await this.cacheService.get(CommonConstants.CACHE_APIKEY_KEY); - this.logger.log(`cachedApiKey----${agentApiKey}`); - if (!agentApiKey || null === agentApiKey || undefined === agentApiKey) { - agentApiKey = await this._getOrgAgentApiKey(getAgentDetails.orgId); - } - payload.apiKey = agentApiKey; + private async generateOOBProofReq(payload: IProofRequestPayload): Promise { const getProofPresentation = await this._sendOutOfBandProofRequest(payload); - this.logger.log(`-----getProofPresentation---${JSON.stringify(getProofPresentation)}`); - if (!getProofPresentation) { throw new Error(ResponseMessages.verification.error.proofPresentationNotFound); } @@ -413,57 +438,45 @@ export class VerificationService { } - async sendEmailInBatches(payload: IProofRequestPayload, emailIds: string[] | string, getAgentDetails: org_agents, organizationDetails: organisation, batchSize: number): Promise { + // Currently batch size is not used, as length of emails sent is restricted to '10' + async sendEmailInBatches(payload: IProofRequestPayload, emailIds: string[], getAgentDetails: org_agents, organizationDetails: organisation): Promise { + try { const accumulatedErrors = []; - if (Array.isArray(emailIds)) { - - for (let i = 0; i < emailIds.length; i += batchSize) { - const batch = emailIds.slice(i, i + batchSize); - const emailPromises = batch.map(async email => { + for (const email of emailIds) { try { - await this.sendOutOfBandProofRequest(payload, email, getAgentDetails, organizationDetails); - } catch (error) { - accumulatedErrors.push(error); - } - }); - - await Promise.all(emailPromises); - } - } else { - await this.sendOutOfBandProofRequest(payload, emailIds, getAgentDetails, organizationDetails); - } + await this.sendOutOfBandProofRequest(payload, email, getAgentDetails, organizationDetails); + await this.delay(500); + } catch (error) { + this.logger.error(`Error sending email to ${email}::::::`, error); + accumulatedErrors.push(error); + } + } if (0 < accumulatedErrors.length) { this.logger.error(accumulatedErrors); throw new Error(ResponseMessages.verification.error.emailSend); } + + } catch (error) { + this.logger.error('[sendEmailInBatches] - error in sending email in batches'); + throw new Error(ResponseMessages.verification.error.batchEmailSend); + } } + // This function is specifically for OOB verification using email async sendOutOfBandProofRequest(payload: IProofRequestPayload, email: string, getAgentDetails: org_agents, organizationDetails: organisation): Promise { - let agentApiKey: string = await this.cacheService.get(CommonConstants.CACHE_APIKEY_KEY); - this.logger.log(`cachedApiKey----${agentApiKey}`); - if (!agentApiKey || null === agentApiKey || undefined === agentApiKey) { - agentApiKey = await this._getOrgAgentApiKey(getAgentDetails.orgId); - } - payload.apiKey = agentApiKey; const getProofPresentation = await this._sendOutOfBandProofRequest(payload); if (!getProofPresentation) { throw new Error(ResponseMessages.verification.error.proofPresentationNotFound); } - const invitationId = getProofPresentation?.response?.invitation['@id']; - - if (!invitationId) { - throw new Error(ResponseMessages.verification.error.invitationNotFound); - } - - const shortenedUrl = getAgentDetails?.tenantId - ? `${getAgentDetails?.agentEndPoint}/multi-tenancy/url/${getAgentDetails?.tenantId}/${invitationId}` - : `${getAgentDetails?.agentEndPoint}/url/${invitationId}`; - + const invitationUrl = getProofPresentation?.response?.invitationUrl; + // Currently have shortenedUrl to store only for 30 days + const persist: boolean = false; + const shortenedUrl = await this.storeVerificationObjectAndReturnUrl(invitationUrl, persist); const qrCodeOptions: QRCode.QRCodeToDataURLOptions = { type: 'image/png' }; const outOfBandVerificationQrCode = await QRCode.toDataURL(shortenedUrl, qrCodeOptions); @@ -476,7 +489,7 @@ export class VerificationService { this.emailData.emailFrom = platformConfigData.emailFrom; this.emailData.emailTo = email; this.emailData.emailSubject = `${process.env.PLATFORM_NAME} Platform: Verification of Your Credentials`; - this.emailData.emailHtml = await this.outOfBandVerification.outOfBandVerification(email, organizationDetails.name, outOfBandVerificationQrCode); + this.emailData.emailHtml = await this.outOfBandVerification.outOfBandVerification(email, organizationDetails.name, shortenedUrl); this.emailData.emailAttachments = [ { filename: 'qrcode.png', @@ -522,33 +535,46 @@ export class VerificationService { requestedPredicates; }> { try { - let requestedAttributes = {}; + let requestedAttributes = {}; const requestedPredicates = {}; - const attributeWithSchemaIdExists = proofRequestpayload.attributes; - if (attributeWithSchemaIdExists) { - requestedAttributes = Object.fromEntries(proofRequestpayload.attributes.map((attribute, index) => { - - const attributeElement = attribute.attributeName; + const { attributes } = proofRequestpayload; + if (attributes) { + requestedAttributes = Object.fromEntries(attributes.map((attribute, index) => { + const attributeElement = attribute.attributeName || attribute.attributeNames; const attributeReferent = `additionalProp${index + 1}`; + const attributeKey = attribute.attributeName ? 'name' : 'names'; + if (!attribute.condition && !attribute.value) { - + return [ attributeReferent, { - name: attributeElement + [attributeKey]: attributeElement, + restrictions: [ + { + cred_def_id: proofRequestpayload.attributes[index].credDefId ? proofRequestpayload.attributes[index].credDefId : undefined, + schema_id: proofRequestpayload.attributes[index].schemaId + } + ] } ]; } else { requestedPredicates[attributeReferent] = { p_type: attribute.condition, name: attributeElement, - p_value: parseInt(attribute.value) + p_value: parseInt(attribute.value), + restrictions: [ + { + cred_def_id: proofRequestpayload.attributes[index].credDefId ? proofRequestpayload.attributes[index].credDefId : undefined, + schema_id: proofRequestpayload.attributes[index].schemaId + } + ] }; } - + return [attributeReferent]; })); - + return { requestedAttributes, requestedPredicates @@ -558,10 +584,10 @@ export class VerificationService { } } catch (error) { this.logger.error(`[proofRequestPayload] - error in proof request payload : ${JSON.stringify(error)}`); - throw new RpcException(error.response ? error.response : error); - - } - } + throw new RpcException(error.response ? error.response : error); + + } + } /** * Description: Fetch agent url @@ -660,38 +686,47 @@ export class VerificationService { try { const getAgentDetails = await this.verificationRepository.getAgentEndPoint(orgId); const verificationMethodLabel = 'get-verified-proof'; - + let credDefId; + let schemaId; const orgAgentType = await this.verificationRepository.getOrgAgentType(getAgentDetails?.orgAgentTypeId); const url = await this.getAgentUrl(verificationMethodLabel, orgAgentType, getAgentDetails?.agentEndPoint, getAgentDetails?.tenantId, '', proofId); - let apiKey: string = await this.cacheService.get(CommonConstants.CACHE_APIKEY_KEY); - this.logger.log(`cachedApiKey----${apiKey}`); - if (!apiKey || null === apiKey || undefined === apiKey) { - apiKey = await this._getOrgAgentApiKey(orgId); - } - const payload = { apiKey, url }; + + const payload = { orgId, url }; const getProofPresentationById = await this._getVerifiedProofDetails(payload); + if (!getProofPresentationById?.response?.presentation) { throw new NotFoundException(ResponseMessages.verification.error.proofPresentationNotFound, { - cause: new Error(), - description: ResponseMessages.errorMessages.notFound + cause: new Error(), + description: ResponseMessages.errorMessages.notFound }); - } + } + const requestedAttributes = getProofPresentationById?.response?.request?.indy?.requested_attributes; const requestedPredicates = getProofPresentationById?.response?.request?.indy?.requested_predicates; const revealedAttrs = getProofPresentationById?.response?.presentation?.indy?.requested_proof?.revealed_attrs; - const extractedDataArray: IProofPresentationDetails[] = []; - if (requestedAttributes && requestedPredicates) { + const extractedDataArray: IProofPresentationDetails[] = []; + if (0 !== Object.keys(requestedAttributes).length && 0 !== Object.keys(requestedPredicates).length) { for (const key in requestedAttributes) { if (requestedAttributes.hasOwnProperty(key)) { const requestedAttributeKey = requestedAttributes[key]; const attributeName = requestedAttributeKey.name; - const credDefId = requestedAttributeKey?.restrictions[0]?.cred_def_id; - const schemaId = requestedAttributeKey?.restrictions[0]?.schema_id; + + if (requestedAttributeKey?.restrictions) { + + credDefId = requestedAttributeKey?.restrictions[0]?.cred_def_id; + schemaId = requestedAttributeKey?.restrictions[0]?.schema_id; + + } else if (getProofPresentationById?.response?.presentation?.indy?.identifiers) { + + credDefId = getProofPresentationById?.response?.presentation?.indy?.identifiers[0].cred_def_id; + schemaId = getProofPresentationById?.response?.presentation?.indy?.identifiers[0].schema_id; + + } if (revealedAttrs.hasOwnProperty(key)) { const extractedData: IProofPresentationDetails = { @@ -705,11 +740,16 @@ export class VerificationService { } for (const key in requestedPredicates) { + if (requestedPredicates.hasOwnProperty(key)) { const attribute = requestedPredicates[key]; + const attributeName = attribute?.name; - const credDefId = attribute?.restrictions[0]?.cred_def_id; - const schemaId = attribute?.restrictions[0]?.schema_id; + + if (attribute?.restrictions) { + credDefId = attribute?.restrictions[0]?.cred_def_id; + schemaId = attribute?.restrictions[0]?.schema_id; + } const extractedData: IProofPresentationDetails = { [attributeName]: `${attribute?.p_type}${attribute?.p_value}`, @@ -720,14 +760,17 @@ export class VerificationService { } } - } else if (requestedAttributes) { + } else if (0 !== Object.keys(requestedAttributes).length) { + for (const key in requestedAttributes) { if (requestedAttributes.hasOwnProperty(key)) { const attribute = requestedAttributes[key]; const attributeName = attribute.name; - const credDefId = attribute?.restrictions[0]?.cred_def_id; - const schemaId = attribute?.restrictions[0]?.schema_id; + + + [credDefId, schemaId] = await this._schemaCredDefRestriction(attribute, getProofPresentationById); + if (revealedAttrs.hasOwnProperty(key)) { const extractedData: IProofPresentationDetails = { @@ -739,17 +782,17 @@ export class VerificationService { } } } - } else if (requestedPredicates) { + } else if (0 !== Object.keys(requestedPredicates).length) { + for (const key in requestedPredicates) { if (requestedPredicates.hasOwnProperty(key)) { const attribute = requestedPredicates[key]; const attributeName = attribute?.name; - const credDefId = attribute?.restrictions[0]?.cred_def_id; - const schemaId = attribute?.restrictions[0]?.schema_id; + [credDefId, schemaId] = await this._schemaCredDefRestriction(attribute, getProofPresentationById); const extractedData: IProofPresentationDetails = { - [attributeName]: `${requestedPredicates?.p_type}${requestedPredicates?.p_value}`, + [attributeName]: `${attribute?.p_type}${attribute?.p_value}`, 'credDefId': credDefId || null, 'schemaId': schemaId || null }; @@ -758,25 +801,42 @@ export class VerificationService { } } else { throw new InternalServerErrorException(ResponseMessages.errorMessages.serverError, { - cause: new Error(), - description: ResponseMessages.errorMessages.serverError + cause: new Error(), + description: ResponseMessages.errorMessages.serverError }); } return extractedDataArray; } catch (error) { - this.logger.error(`[getVerifiedProofDetails] - error in get verified proof details : ${JSON.stringify(error)}`); - const errorStack = error?.response?.error?.reason; - - if (errorStack) { - throw new RpcException({ - message: ResponseMessages.verification.error.verifiedProofNotFound, - statusCode: error?.response?.status, - error: errorStack - }); + this.logger.error(`[getVerifiedProofDetails] - error in get verified proof details : ${JSON.stringify(error)}`); + const errorStack = error?.response?.error?.reason; + + if (errorStack) { + throw new RpcException({ + message: ResponseMessages.verification.error.verifiedProofNotFound, + statusCode: error?.response?.status, + error: errorStack + }); } else { - throw new RpcException(error.response ? error.response : error); - } + throw new RpcException(error.response ? error.response : error); } + } + } + + async _schemaCredDefRestriction(attribute, getProofPresentationById): Promise { + let credDefId; + let schemaId; + + if (attribute?.restrictions) { + + credDefId = attribute?.restrictions[0]?.cred_def_id; + schemaId = attribute?.restrictions[0]?.schema_id; + } else if (getProofPresentationById?.response?.presentation?.indy?.identifiers) { + + credDefId = getProofPresentationById?.response?.presentation?.indy?.identifiers[0].cred_def_id; + schemaId = getProofPresentationById?.response?.presentation?.indy?.identifiers[0].schema_id; + } + + return [credDefId, schemaId]; } async _getVerifiedProofDetails(payload: IVerifiedProofData): Promise<{ @@ -816,7 +876,7 @@ export class VerificationService { verificationErrorHandling(error): void { if (!error && !error?.status && !error?.status?.message && !error?.status?.message?.error) { - + throw new RpcException(error.response ? error.response : error); } else { throw new RpcException({ @@ -848,4 +908,7 @@ export class VerificationService { }); } -} \ No newline at end of file + async delay(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); + } +} \ No newline at end of file diff --git a/apps/verification/templates/out-of-band-verification.template.ts b/apps/verification/templates/out-of-band-verification.template.ts index 858635ad2..cbe4d4af2 100644 --- a/apps/verification/templates/out-of-band-verification.template.ts +++ b/apps/verification/templates/out-of-band-verification.template.ts @@ -1,6 +1,6 @@ export class OutOfBandVerification { - public outOfBandVerification(email: string, orgName: string, verificationQrCode: string): string { + public outOfBandVerification(email: string, orgName: string, shortenedUrl: string): string { try { return ` @@ -14,7 +14,7 @@ export class OutOfBandVerification {
- CREDEBL logo + ${process.env.PLATFORM_NAME} logo
@@ -24,16 +24,16 @@ export class OutOfBandVerification {

${orgName} has requested verification of your digital credential. To share requested credential kindly follow below steps:

    -
  • Download the ADEYA SSI App from - Android Play Store or -iOS App Store. (Skip, if already downloaded) +
  • Download the ${process.env.MOBILE_APP_NAME} from + Android Play Store or +iOS App Store. (Skip, if already downloaded)
  • -
  • Complete the onboarding process in ADEYA.
  • -
  • Open the “Share Credential” link below in this email (This will open the link in the ADEYA App)
  • -
  • Tap the "Send Proof" button in ADEYA to share you credential data.
  • +
  • Complete the onboarding process in ${process.env.MOBILE_APP}.
  • +
  • Open the “Share Credential” link below in this email (This will open the link in the ${process.env.MOBILE_APP} App)
  • +
  • Tap the "Send Proof" button in ${process.env.MOBILE_APP} to share you credential data.
- Share Credential @@ -41,7 +41,7 @@ export class OutOfBandVerification {

- Note: If the above steps do not work for you, please open the attached QR Code image in this email on another device, and scan the QR code using the ADEYA SSI App on your mobile device. + Note: If the above steps do not work for you, please open the attached QR Code image in this email on another device, and scan the QR code using the ${process.env.MOBILE_APP_NAME} on your mobile device. The QR Code is single-use.

@@ -50,11 +50,11 @@ export class OutOfBandVerification {
- For any assistance or questions while accessing your account, please do not hesitate to contact the support team at support@blockster.global. Our team will ensure a seamless onboarding experience for you. + For any assistance or questions while accessing your account, please do not hesitate to contact the support team at ${process.env.PUBLIC_PLATFORM_SUPPORT_EMAIL}. Our team will ensure a seamless onboarding experience for you.

- © Blockster Labs Pvt. Ltd. + © ${process.env.POWERED_BY}

diff --git a/docker-compose.yml b/docker-compose.yml index b05c9ab25..07d9fd9c8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -111,9 +111,17 @@ services: - verification build: context: ./ # Adjust the context path as needed - dockerfile: Dockerfiles/Dockerfile.agnet-provisioning + dockerfile: Dockerfiles/Dockerfile.agent-provisioning + args: + - ROOT_PATH=$PWD/apps/agent-provisioning/AFJ/agent-config env_file: - ./.env + environment: + - ROOT_PATH=$PWD/apps/agent-provisioning/AFJ/agent-config + volumes: + - $PWD/apps/agent-provisioning/AFJ/agent-config:/app/agent-provisioning/AFJ/agent-config + - /var/run/docker.sock:/var/run/docker.sock + - /app/agent-provisioning/AFJ/token:/app/agent-provisioning/AFJ/token agent-service: depends_on: - nats # Use depends_on instead of needs @@ -125,11 +133,16 @@ services: - organization - verification - agent-provisioning + command: sh -c 'until (docker logs platform-agent-provisioning-1 | grep "Agent-Provisioning-Service Microservice is listening to NATS"); do sleep 1; done && node dist/apps/agent-service/main.js' build: context: ./ # Adjust the context path as needed dockerfile: Dockerfiles/Dockerfile.agent-service env_file: - ./.env + volumes: + - /var/run/docker.sock:/var/run/docker.sock + volumes_from: + - agent-provisioning diff --git a/libs/aws/src/aws.service.ts b/libs/aws/src/aws.service.ts index 29b067cdf..f9c7cb7e4 100644 --- a/libs/aws/src/aws.service.ts +++ b/libs/aws/src/aws.service.ts @@ -7,6 +7,7 @@ import { promisify } from 'util'; export class AwsService { private s3: S3; private s4: S3; + private s3StoreObject: S3; constructor() { this.s3 = new S3({ @@ -16,19 +17,24 @@ export class AwsService { }); this.s4 = new S3({ - accessKeyId: process.env.AWS_PUBLIC_ACCESS_KEY, secretAccessKey: process.env.AWS_PUBLIC_SECRET_KEY, region: process.env.AWS_PUBLIC_REGION }); + + this.s3StoreObject = new S3({ + accessKeyId: process.env.AWS_S3_STOREOBJECT_ACCESS_KEY, + secretAccessKey: process.env.AWS_S3_STOREOBJECT_SECRET_KEY, + region: process.env.AWS_S3_STOREOBJECT_REGION + }); } - + async uploadUserCertificate( fileBuffer: Buffer, ext: string, filename: string, bucketName: string, - encoding : string, + encoding: string, pathAWS: string = '' ): Promise { const timestamp = Date.now(); @@ -43,8 +49,8 @@ export class AwsService { ContentType: `image/png` }); - const imageUrl = `https://${process.env.AWS_ORG_LOGO_BUCKET_NAME}.s3.${process.env.AWS_PUBLIC_REGION}.amazonaws.com/${pathAWS}/${encodeURIComponent(filename)}-${timestamp}.${ext}`; - return imageUrl; + const imageUrl = `https://${process.env.AWS_ORG_LOGO_BUCKET_NAME}.s3.${process.env.AWS_PUBLIC_REGION}.amazonaws.com/${pathAWS}/${encodeURIComponent(filename)}-${timestamp}.${ext}`; + return imageUrl; } catch (error) { throw new HttpException(error, HttpStatus.SERVICE_UNAVAILABLE); } @@ -87,4 +93,23 @@ export class AwsService { throw new RpcException(error.response ? error.response : error); } } + + async storeObject(persistent: boolean, key: string, body: unknown): Promise { + const objKey: string = persistent.valueOf() ? `persist/${key}` : `default/${key}`; + const buf = Buffer.from(JSON.stringify(body)); + const params: AWS.S3.PutObjectRequest = { + Bucket: process.env.AWS_S3_STOREOBJECT_BUCKET, + Body: buf, + Key: objKey, + ContentEncoding: 'base64', + ContentType: 'application/json' + }; + + try { + const receivedData = await this.s3StoreObject.upload(params).promise(); + return receivedData; + } catch (error) { + throw new RpcException(error.response ? error.response : error); + } + } } diff --git a/libs/client-registration/src/client-registration.service.ts b/libs/client-registration/src/client-registration.service.ts index 7766e0926..7c3b0ede9 100644 --- a/libs/client-registration/src/client-registration.service.ts +++ b/libs/client-registration/src/client-registration.service.ts @@ -18,6 +18,7 @@ import { userTokenPayloadDto } from './dtos/userTokenPayloadDto'; import { KeycloakUserRegistrationDto } from 'apps/user/dtos/keycloak-register.dto'; import { ResponseMessages } from '@credebl/common/response-messages'; import { ResponseService } from '@credebl/response'; +import { IClientRoles } from './interfaces/client.interface'; @Injectable() export class ClientRegistrationService { @@ -164,7 +165,6 @@ export class ClientRegistrationService { `${process.env.KEYCLOAK_DOMAIN}admin/realms/${process.env.KEYCLOAK_REALM}/users/${payload['sub']}`, this.getAuthHeader(token) ); - this.logger.debug(`keycloak user ${JSON.stringify(userInfoResponse)}`); return userInfoResponse.data; } catch (error) { this.logger.error(`[getUserInfo]: ${JSON.stringify(error)}`); @@ -177,16 +177,15 @@ export class ClientRegistrationService { const payload = new ClientCredentialTokenPayloadDto(); payload.client_id = process.env.KEYCLOAK_MANAGEMENT_CLIENT_ID; payload.client_secret = process.env.KEYCLOAK_MANAGEMENT_CLIENT_SECRET; - payload.scope = 'email profile'; const mgmtTokenResponse = await this.getToken(payload); return mgmtTokenResponse.access_token; } catch (error) { + this.logger.error(`Error in getManagementToken: ${JSON.stringify(error)}`); throw error; } } - async getManagementTokenForMobile() { try { const payload = new ClientCredentialTokenPayloadDto(); @@ -235,7 +234,6 @@ export class ClientRegistrationService { this.getAuthHeader(token) ); - this.logger.debug(`Existing apps response ${JSON.stringify(response)}`); return { clientId: client_id, @@ -277,6 +275,205 @@ export class ClientRegistrationService { } + async createUserClientRole( + idpId: string, + token: string, + userId: string, + payload: object[] + ): Promise { + + const realmName = process.env.KEYCLOAK_REALM; + + const createClientRolesResponse = await this.commonService.httpPost( + await this.keycloakUrlService.GetClientUserRoleURL(realmName, userId, idpId), + payload, + this.getAuthHeader(token) + ); + + this.logger.debug( + `createUserClientRolesResponse ${JSON.stringify( + createClientRolesResponse + )}` + ); + + return 'User client role is assigned'; + } + + async deleteUserClientRoles( + idpId: string, + token: string, + userId: string + ): Promise { + + const realmName = process.env.KEYCLOAK_REALM; + + const createClientRolesResponse = await this.commonService.httpDelete( + await this.keycloakUrlService.GetClientUserRoleURL(realmName, userId, idpId), + this.getAuthHeader(token) + ); + + this.logger.debug( + `deleteUserClientRoles ${JSON.stringify( + createClientRolesResponse + )}` + ); + + return 'User client role is deleted'; + } + + async createUserHolderRole( + token: string, + userId: string, + payload: object[] + ): Promise { + + const realmName = process.env.KEYCLOAK_REALM; + + const createClientRolesResponse = await this.commonService.httpPost( + await this.keycloakUrlService.GetClientUserRoleURL(realmName, userId), + payload, + this.getAuthHeader(token) + ); + + this.logger.debug( + `createUserHolderRole ${JSON.stringify( + createClientRolesResponse + )}` + ); + + return 'User holder role is assigned'; + } + + async getAllClientRoles( + idpId: string, + token: string + ): Promise { + + const realmName = process.env.KEYCLOAK_REALM; + + const clientRolesResponse = await this.commonService.httpGet( + await this.keycloakUrlService.GetClientRoleURL(realmName, idpId), + this.getAuthHeader(token) + ); + + this.logger.debug( + `getAllClientRoles ${JSON.stringify( + clientRolesResponse + )}` + ); + + return clientRolesResponse; + } + + async getClientSpecificRoles( + idpId: string, + token: string, + roleName: string + ): Promise { + + const realmName = process.env.KEYCLOAK_REALM; + + const clientRolesResponse = await this.commonService.httpGet( + await this.keycloakUrlService.GetClientRoleURL(realmName, idpId, roleName), + this.getAuthHeader(token) + ); + + this.logger.debug( + `getClientSpecificRoles ${JSON.stringify( + clientRolesResponse + )}` + ); + + return clientRolesResponse; + } + + async getAllRealmRoles( + token: string + ): Promise { + + const realmName = process.env.KEYCLOAK_REALM; + + const realmRolesResponse = await this.commonService.httpGet( + await this.keycloakUrlService.GetRealmRoleURL(realmName), + this.getAuthHeader(token) + ); + + this.logger.debug( + `getAllRealmRoles ${JSON.stringify( + realmRolesResponse + )}` + ); + + return realmRolesResponse; + } + + + async createClientRole( + idpId: string, + token: string, + name: string, + description: string + ): Promise { + + const payload = { + clientRole: true, + name, + description + }; + + const realmName = process.env.KEYCLOAK_REALM; + + const createClientRolesResponse = await this.commonService.httpPost( + await this.keycloakUrlService.GetClientRoleURL(realmName, idpId), + payload, + this.getAuthHeader(token) + ); + + this.logger.debug( + `createClientRolesResponse ${JSON.stringify( + createClientRolesResponse + )}` + ); + + return 'Client role is created'; + + } + + async generateClientSecret( + idpId: string, + token: string + ): Promise { + + const realmName = process.env.KEYCLOAK_REALM; + + const createClientSercretResponse = await this.commonService.httpPost( + await this.keycloakUrlService.GetClientSecretURL(realmName, idpId), + {}, + this.getAuthHeader(token) + ); + + this.logger.debug( + `ClientRegistrationService create realm client secret ${JSON.stringify( + createClientSercretResponse + )}` + ); + + const getClientSercretResponse = await this.commonService.httpGet( + await this.keycloakUrlService.GetClientSecretURL(realmName, idpId), + this.getAuthHeader(token) + ); + this.logger.debug( + `ClientRegistrationService get client secret ${JSON.stringify( + getClientSercretResponse + )}` + ); + this.logger.log(`${getClientSercretResponse.value}`); + const clientSecret = getClientSercretResponse.value; + + return clientSecret; + + } + async createClient( orgName: string, orgId: string, @@ -324,20 +521,7 @@ export class ClientRegistrationService { publicClient: false, frontchannelLogout: false, fullScopeAllowed: false, - nodeReRegistrationTimeout: 0, - defaultClientScopes: [ - 'web-origins', - 'role_list', - 'profile', - 'roles', - 'email' - ], - optionalClientScopes: [ - 'address', - 'phone', - 'offline_access', - 'microprofile-jwt' - ] + nodeReRegistrationTimeout: 0 }; const createClientResponse = await this.commonService.httpPost( @@ -471,7 +655,6 @@ export class ClientRegistrationService { async getToken(payload: ClientCredentialTokenPayloadDto) { try { - this.logger.log(`getting token : ${JSON.stringify(payload)}`); if ( 'client_credentials' !== payload.grant_type || !payload.client_id || @@ -491,9 +674,6 @@ export class ClientRegistrationService { qs.stringify(payload) , config); - this.logger.debug( - `ClientRegistrationService token ${JSON.stringify(tokenResponse)}` - ); return tokenResponse; } catch (error) { throw error; @@ -566,9 +746,6 @@ export class ClientRegistrationService { payload.username = email; payload.password = password; - this.logger.log(`User Token Payload: ${JSON.stringify(payload)}`); - - if ( 'password' !== payload.grant_type || !payload.client_id || @@ -593,9 +770,6 @@ export class ClientRegistrationService { qs.stringify(payload) , config); - this.logger.debug( - `ClientRegistrationService token ${JSON.stringify(tokenResponse)}` - ); return tokenResponse; } catch (error) { @@ -612,10 +786,6 @@ export class ClientRegistrationService { payload.refresh_token = refreshToken; payload.client_secret = process.env.KEYCLOAK_MANAGEMENT_CLIENT_SECRET; - - this.logger.log(`access Token for platform Payload: ${JSON.stringify(payload)}`); - - if ( 'refresh_token' !== payload.grant_type || !payload.client_id || @@ -626,8 +796,6 @@ export class ClientRegistrationService { throw new Error('Invalid inputs while getting token.'); } - const strURL = await this.keycloakUrlService.GetSATURL('credebl-platform'); - this.logger.log(`getToken URL: ${strURL}`); const config = { headers: { 'Content-Type': 'application/x-www-form-urlencoded' @@ -635,17 +803,16 @@ export class ClientRegistrationService { }; const tokenResponse = await this.commonService.httpPost( - await this.keycloakUrlService.GetSATURL('credebl-platform'), + await this.keycloakUrlService.GetSATURL(process.env.KEYCLOAK_REALM), qs.stringify(payload) , config); - this.logger.debug( - `ClientRegistrationService token ${JSON.stringify(tokenResponse)}` - ); return tokenResponse; } catch (error) { - + this.logger.error( + `Error in getAccessToken ${JSON.stringify(error)}` + ); throw error; } } diff --git a/libs/client-registration/src/interfaces/client.interface.ts b/libs/client-registration/src/interfaces/client.interface.ts new file mode 100644 index 000000000..59e78a52c --- /dev/null +++ b/libs/client-registration/src/interfaces/client.interface.ts @@ -0,0 +1,8 @@ +export interface IClientRoles { + id: string + name: string + description?: string + composite?: boolean + clientRole?: boolean + containerId?: string +} \ No newline at end of file diff --git a/libs/common/src/cast.helper.ts b/libs/common/src/cast.helper.ts index 4b886c1ef..d92cb2a59 100644 --- a/libs/common/src/cast.helper.ts +++ b/libs/common/src/cast.helper.ts @@ -44,4 +44,15 @@ export function toNumber(value: string, opts: ToNumberOptions = {}): number { } return newValue; +} + +export function ledgerName(value: string): string { + let network; + network = value.replace(":", " "); + network = network.charAt(0).toUpperCase() + network.slice(1); + const words = network.split(" "); + network = `${words[0]} ${words[1].charAt(0).toUpperCase()}${words[1].slice(1)}`; + + return network; + } \ No newline at end of file diff --git a/libs/common/src/common.constant.ts b/libs/common/src/common.constant.ts index abeeebf8f..ebb8f4171 100644 --- a/libs/common/src/common.constant.ts +++ b/libs/common/src/common.constant.ts @@ -23,6 +23,7 @@ export enum CommonConstants { URL_CONN_REMOVE_CONNECTION_BY_ID = '/connections/#/remove', URL_CONN_METADATA = '/connections/#/metadata', URL_CONN_LEGACY_INVITE = '/oob/create-legacy-invitation', + URL_CONN_INVITE = '/oob/create-invitation', URL_RECEIVE_INVITATION_URL = '/oob/receive-invitation-url', URL_RECEIVE_INVITATION = '/oob/receive-invitation', URL_CONN_INVITATION = '/url', @@ -73,7 +74,8 @@ export enum CommonConstants { URL_ISSUE_GET_CREDS_AFJ_BY_CRED_REC_ID = '/credentials', URL_OUT_OF_BAND_CREDENTIAL_OFFER = '/credentials/create-offer-oob', URL_ACCEPT_CREDENTIALS = '/credentials/accept-offer', - + URL_SEND_QUESTION = '/question-answer/question/#', + URL_QUESTION_ANSWER_RECORD = '/question-answer', // SCHEMA & CRED DEF SERVICES URL_SCHM_CREATE_SCHEMA = '/schemas', @@ -83,14 +85,20 @@ export enum CommonConstants { URL_SCHM_GET_CRED_DEF_BY_ID = '/credential-definitions/#', URL_SCHM_GET_CRED_DEF_BY_ATTRB = '/credential-definitions/created', + // POLYGON BASED W3C SCHEMAS + DEDICATED_CREATE_POLYGON_W3C_SCHEMA = '/polygon/create-schema', + SHARED_CREATE_POLYGON_W3C_SCHEMA = '/multi-tenancy/polygon-wc3/schema/', + // SHARED AGENT URL_SHAGENT_CREATE_TENANT = '/multi-tenancy/create-tenant', + URL_SHAGENT_CREATE_DID = '/multi-tenancy/create-did/', URL_SHAGENT_WITH_TENANT_AGENT = '/multi-tenancy/with-tenant-agent', URL_SHAGENT_CREATE_SCHEMA = '/multi-tenancy/schema/#', URL_SHAGENT_GET_SCHEMA = '/multi-tenancy/schema/@/#', URL_SHAGENT_CREATE_CRED_DEF = '/multi-tenancy/credential-definition/#', URL_SHAGENT_GET_CRED_DEF = '/multi-tenancy/credential-definition/@/#', URL_SHAGENT_CREATE_INVITATION = '/multi-tenancy/create-legacy-invitation/#', + URL_SHAGENT_CREATE_CONNECTION_INVITATION = '/multi-tenancy/create-invitation/#', URL_SHAGENT_GET_CREATEED_INVITATIONS = '/multi-tenancy/connections/#', URL_SHAGENT_GET_CREATEED_INVITATION_BY_CONNECTIONID = '/multi-tenancy/connections/#/@', URL_SHAGENT_CREATE_OFFER = '/multi-tenancy/credentials/create-offer/#', @@ -106,8 +114,10 @@ export enum CommonConstants { URL_SHAGENT_ACCEPT_OFFER = '/multi-tenancy/credentials/accept-offer/#', URL_SHAGENT_RECEIVE_INVITATION_URL = '/multi-tenancy/receive-invitation-url/#', URL_SHAGENT_RECEIVE_INVITATION = '/multi-tenancy/receive-invitation/#', - - + URL_SHAGENT_SEND_QUESTION = '/multi-tenancy/question-answer/question/#/@', + URL_SHAGENT_SEND_ANSWER = '/multi-tenancy/question-answer/answer/#/@', + URL_SHAGENT_QUESTION_ANSWER_RECORD = '/multi-tenancy/question-answer/#', + // PROOF SERVICES URL_SEND_PROOF_REQUEST = '/proofs/request-proof', URL_GET_PROOF_PRESENTATIONS = '/proofs', @@ -119,10 +129,13 @@ export enum CommonConstants { // server or agent URL_SERVER_STATUS = '/status', URL_AGENT_WRITE_DID = '/dids/write', - URL_AGENT_GET_DID = '/dids/#', - URL_AGENT_GET_DIDS = '/dids', + URL_AGENT_GET_DID = '/dids', URL_AGENT_GET_ENDPOINT = '/agent', + // CREATE KEYS + CREATE_POLYGON_SECP256k1_KEY = '/polygon/create-keys', + + // ENTITY NAMES ENTITY_NAME_TEMPLATE = 'templates', ENTITY_NAME_CRED_DEF = 'credential_definition', @@ -257,14 +270,6 @@ export enum CommonConstants { ACTIVE_NON_ADMIN_USER = 1, ALL_NON_ADMIN_USER = 3, - //passwordLess-login - PASSWORDLESS_LOGIN_SCHEMA_ORG = 1, - PASSWORDLESS_LOGIN_SCHEMA_NAME = 'CREDEBL-PLA', - PLATFORM_ADMIN_CRED_DEF_NAME = 'CREDEBL-PLA', - PLATFORM_ADMIN_SCHEMA_VERSION = '1.0', - - LOGIN_PASSWORDLESS = 'passwordless', - LOGIN_PASSWORD = 'password', // Platform admin Details PLATFORM_ADMIN_EMAIL='platform.admin@yopmail.com', @@ -306,9 +311,16 @@ export enum CommonConstants { TRANSACTION_MULTITENANT_SIGN = '/multi-tenancy/transactions/endorse/#', TRANSACTION_MULTITENANT_SUMBIT = '/multi-tenancy/transactions/write/#', + // Static values to up platform Agent + SEED = '101111110111101100111100000Seed1', + KEYTYPE = 'ed25519', + METHOD = 'indy', + NETWORK = 'bcovrin:testnet', + ROLE = 'endorser', //CacheInfo -CACHE_APIKEY_KEY = "apiKey", +CACHE_SHARED_APIKEY_KEY = "dedicatedApiKey", +CACHE_APIKEY_KEY = "sharedApiKey", CACHE_TTL_SECONDS = 604800 } @@ -767,4 +779,4 @@ export const DISALLOWED_EMAIL_DOMAIN = [ 'zapto.org', 'ze.cx', 'zeroe.ml' -]; +]; \ No newline at end of file diff --git a/libs/common/src/common.service.ts b/libs/common/src/common.service.ts index 5a34f2247..be3f4e183 100644 --- a/libs/common/src/common.service.ts +++ b/libs/common/src/common.service.ts @@ -17,6 +17,8 @@ import { import { CommonConstants } from './common.constant'; import { HttpService } from '@nestjs/axios/dist'; import { ResponseService } from '@credebl/response'; +import * as dotenv from 'dotenv'; +dotenv.config(); @Injectable() export class CommonService { @@ -27,16 +29,10 @@ export class CommonService { async httpPost(url: string, payload?: any, apiKey?: any) { try { - this.logger.debug( - `httpPost service: URL : ${url} \nAPI KEY : ${JSON.stringify( - apiKey - )} \nPAYLOAD : ${JSON.stringify(payload)}` - ); return await this.httpService .post(url, payload, apiKey) .toPromise() .then((response: any) => { - this.logger.error(response.data); return response.data; }); } catch (error) { @@ -99,7 +95,6 @@ export class CommonService { async httpGet(url: string, config?: any) { try { - this.logger.debug(`httpGet service URL: ${url}`); return await this.httpService .get(url, config) .toPromise() @@ -163,11 +158,6 @@ export class CommonService { async httpPatch(url: string, payload?: any, apiKey?: any) { try { - this.logger.debug( - `httpPatch service: URL : ${url} \nAPI KEY : ${JSON.stringify( - apiKey - )} \nPAYLOAD : ${JSON.stringify(payload)}` - ); return await this.httpService .patch(url, payload, apiKey) .toPromise() @@ -231,7 +221,6 @@ export class CommonService { async httpDelete(url: string, config?: unknown): Promise { try { - this.logger.debug(`httpDelete service URL: ${url}`); return await this.httpService .delete(url, config) .toPromise() @@ -299,11 +288,6 @@ export class CommonService { config?: any ): Promise { try { - this.logger.debug( - `httpPut service: URL : ${url} \nCONFIG : ${JSON.stringify( - config - )} \nPAYLOAD : ${JSON.stringify(payload)}` - ); const response = await this.httpService .put(url, payload, config) .toPromise(); @@ -388,11 +372,11 @@ export class CommonService { encryptedPassword, process.env.CRYPTO_PRIVATE_KEY ); + const decryptedPassword = JSON.parse(password.toString(CryptoJS.enc.Utf8)); return decryptedPassword; } catch (error) { throw new BadRequestException('Invalid Credentials'); } } - } diff --git a/libs/common/src/did.validator.ts b/libs/common/src/did.validator.ts new file mode 100644 index 000000000..cd36579cf --- /dev/null +++ b/libs/common/src/did.validator.ts @@ -0,0 +1,43 @@ +import { DidMethod } from '@credebl/enum/enum'; +import { IDidCreate } from './interfaces/did.interface'; +import { BadRequestException } from '@nestjs/common'; + +export function validateDid(createDid: IDidCreate): void { + const errors = []; + + switch (true) { + case DidMethod.WEB === createDid.method && !createDid.domain: + errors.push('domain is required for Web method'); + break; + case (createDid.method === DidMethod.INDY || createDid.method === DidMethod.POLYGON) && !createDid.network: + errors.push('network is required'); + break; + case (createDid.method === DidMethod.INDY || createDid.method === DidMethod.POLYGON) && 'ed25519' !== createDid.keyType: + errors.push('Only ed25519 key type is supported'); + break; + case (createDid.method === DidMethod.WEB || createDid.method === DidMethod.KEY) && !('ed25519' === createDid.keyType || 'bls12381g2' === createDid.keyType): + errors.push('Only ed25519 and bls12381g2 key type is supported'); + break; + case DidMethod.INDY === createDid.method && !(createDid.role || createDid.endorserDid): + errors.push('role or endorserDid is required'); + break; + case DidMethod.POLYGON === createDid.method && !createDid.privatekey: + errors.push('privatekey is required for polygon method'); + break; + case DidMethod.POLYGON === createDid.method && createDid.privatekey && 64 !== createDid.privatekey.length: + errors.push('Private key must be exactly 64 characters long'); + break; + case DidMethod.POLYGON === createDid.method && !createDid.endpoint: + errors.push('endpoint is required for polygon method'); + break; + case (DidMethod.INDY === createDid.method || DidMethod.KEY === createDid.method || DidMethod.WEB === createDid.method) && (!createDid.seed): + errors.push('seed is required'); + break; + default: + break; + } + + if (0 < errors.length) { + throw new BadRequestException(errors); + } +} diff --git a/libs/common/src/interfaces/agent-service.interface.ts b/libs/common/src/interfaces/agent-service.interface.ts new file mode 100644 index 000000000..235e00210 --- /dev/null +++ b/libs/common/src/interfaces/agent-service.interface.ts @@ -0,0 +1,54 @@ +export interface InvitationMessage { + message: { + invitationUrl: string; + invitation: { + '@type': string; + '@id': string; + label: string; + recipientKeys: string[]; + serviceEndpoint: string; + routingKeys: string[]; + }; + outOfBandRecord: OutOfBandRecord; + recipientKey?:string + }; + } + + interface OutOfBandRecord { + _tags: Tags; + metadata?: { [key: string]: string }; + id: string; + createdAt: string; + outOfBandInvitation: OutOfBandInvitation; + role: string; + state: string; + autoAcceptConnection: boolean; + reusable: boolean; + updatedAt: string; + } + + interface Tags { + invitationId: string; + recipientKeyFingerprints: string[]; + role: string; + state: string; + threadId: string; + } + + interface OutOfBandInvitation { + '@type': string; + '@id': string; + label: string; + accept: string[]; + handshake_protocols: string[]; + services: OutOfBandInvitationService[]; + } + + interface OutOfBandInvitationService { + id: string; + serviceEndpoint: string; + type: string; + recipientKeys: string[]; + routingKeys: string[]; + } + \ No newline at end of file diff --git a/libs/common/src/interfaces/connection.interface.ts b/libs/common/src/interfaces/connection.interface.ts index 0d4a34ca5..9ff7c4785 100644 --- a/libs/common/src/interfaces/connection.interface.ts +++ b/libs/common/src/interfaces/connection.interface.ts @@ -30,5 +30,6 @@ export interface IConnectionsListCount { createdBy: number; lastChangedDateTime: Date; lastChangedBy: number; + recipientKey?:string; } \ No newline at end of file diff --git a/libs/common/src/interfaces/did.interface.ts b/libs/common/src/interfaces/did.interface.ts new file mode 100644 index 000000000..8299f06d7 --- /dev/null +++ b/libs/common/src/interfaces/did.interface.ts @@ -0,0 +1,13 @@ +export interface IDidCreate { + keyType: string; + seed?: string; + domain?: string; + network?: string; + privatekey?: string; + endpoint?: string; + method: string; + did?: string; + role?: string; + endorserDid?: string; + didDocument?: object; +} \ No newline at end of file diff --git a/libs/common/src/interfaces/ecosystem.interface.ts b/libs/common/src/interfaces/ecosystem.interface.ts new file mode 100644 index 000000000..648153c19 --- /dev/null +++ b/libs/common/src/interfaces/ecosystem.interface.ts @@ -0,0 +1,42 @@ +interface EcosystemRole { + id: string; + name: string; + description: string; + createDateTime: Date; + lastChangedDateTime: Date; + deletedAt: Date; + } + +interface Ecosystem { + id: string; + name: string; + description: string; + logoUrl: string | null; + createDateTime: string; + lastChangedDateTime: string; + createdBy: string; + autoEndorsement: boolean; + ecosystemOrgs: EcosystemOrg[]; + } + + interface EcosystemOrg { + id: string; + orgId: string; + status: string; + createDateTime: string; + lastChangedDateTime: string; + ecosystemId: string; + ecosystemRoleId: string; + ecosystemRole: EcosystemRole; + } + + export interface IEcosystemDetails { + totalItems: number; + hasNextPage: boolean; + hasPreviousPage: boolean; + nextPage: number; + previousPage: number; + lastPage: number; + ecosystemList: Ecosystem[]; + } + \ No newline at end of file diff --git a/libs/common/src/response-messages/index.ts b/libs/common/src/response-messages/index.ts index ecea0f3a2..89d149584 100644 --- a/libs/common/src/response-messages/index.ts +++ b/libs/common/src/response-messages/index.ts @@ -23,7 +23,8 @@ export const ResponseMessages = { updateUserProfile:'User profile updated successfully', resetPassword: 'Password reset successfully', degreeCertificate: 'Degree Certificate shared successfully', - resetPasswordLink: 'Reset password link has been sent to your mail' + resetPasswordLink: 'Reset password link has been sent to your mail', + refreshToken: 'Token details fetched successfully' }, error: { exists: 'User already exists', @@ -40,7 +41,7 @@ export const ResponseMessages = { verifyMail: 'Please verify your email', invalidCredentials: 'Invalid Credentials', registerFido: 'Please complete your fido registration', - invitationNotFound: 'Invitation not found', + invitationNotFound: 'Invitation not found for this user', invitationAlreadyAccepted:'Organization invitation already accepted', invitationAlreadyRejected:'Organization invitation already rejected', invalidInvitationStatus: 'Invalid invitation status', @@ -60,7 +61,8 @@ export const ResponseMessages = { resetSamePassword: 'New password should not be the current password', resetPasswordLink: 'Unable to create reset password token', invalidResetLink: 'Invalid or expired reset password link', - invalidAccessToken: 'Authentication failed' + invalidAccessToken: 'Authentication failed', + invalidRefreshToken: 'Invalid refreshToken provided' } }, organisation: { @@ -91,7 +93,7 @@ export const ResponseMessages = { rolesNotExist: 'Provided roles not exists in the platform', orgProfile: 'Organization profile not found', userNotFound: 'User not found for the given organization', - orgRoleIdNotFound:'Provided roles not exists in the platform', + orgRoleIdNotFound:'Provided roles not exists for this organization', updateUserRoles: 'Unable to update user roles', deleteOrg: 'Organization not found', deleteOrgInvitation: 'Organization does not have access to delete this invitation', @@ -107,7 +109,9 @@ export const ResponseMessages = { invalidUserId:'Invalid format of userId', invalidInvitationId:'Invalid format for invitation id', ecosystemIdIsRequired:'ecosystemId is required', - roleNotMatch: 'User does not have access' + roleNotMatch: 'User does not have access', + orgDoesNotMatch: 'Organization does not match', + invalidClient: 'Invalid client credentials' } }, @@ -156,7 +160,9 @@ export const ResponseMessages = { credentialDefinitionNotFound: 'No credential definition exist', notStoredCredential: 'User credential not stored', agentDetailsNotFound: 'Agent details not found', - failedFetchSchema: 'Failed to fetch schema data' + failedFetchSchema: 'Failed to fetch schema data', + atLeastOneRequired: 'At least one of the attributes should have isReuired as `true`', + schemaBuilder: 'Error while creating schema JSON`' } }, credentialDefinition: { @@ -184,9 +190,13 @@ export const ResponseMessages = { agent: { success: { create: 'Agent process initiated successfully. Please wait', + createWallet: 'Wallet created successfully', + createDid: 'Did created successfully', health: 'Agent health details retrieved successfully.', + ledgerConfig: 'Ledger config details fetched successfully.', webhookUrlRegister:'Webhook Url registered successfully', - getWebhookUrl:'Webhook Url fetched successfully' + getWebhookUrl:'Webhook Url fetched successfully', + createKeys:'Key-pair created successfully' }, error: { exists: 'An agent name is already exist', @@ -211,6 +221,7 @@ export const ResponseMessages = { walletAlreadyProcessing: 'Your wallet is already processing', notAbleToSpinp: 'Agent not able to spun up', platformAdminNotAbleToSpinp: 'Platform admin agent is not spun up', + invalidLedger: 'Invalid ledger name', seedCharCount: 'seed must be at most 32 characters', nullTenantId:'TenantId must not be null', tenantIdNotFound:'TenantId not found', @@ -223,7 +234,9 @@ export const ResponseMessages = { create: 'Connection created successfully', receivenvitation: 'Invitation received successfully', fetchConnection: 'Connection details fetched successfully', - fetch: 'Connections details fetched successfully' + fetch: 'Connections details fetched successfully', + questionAnswerRecord: 'Question Answer record fetched successfully', + questionSend:'Question sent successfully' }, error: { exists: 'Connection is already exist', @@ -265,7 +278,10 @@ export const ResponseMessages = { emailIdNotPresent: 'EmailId is empty or not present', attributesNotPresent: 'Attributes are not present or not empty', unableToCreateOffer: 'Unable to create offer', - orgAgentTypeNotFound: 'Organization agent type not found' + orgAgentTypeNotFound: 'Organization agent type not found', + credentialNotPresent: 'credential is required', + optionsNotPresent:'options are required', + invalidCredentialType:'invalid credential type' } }, verification: { @@ -287,6 +303,7 @@ export const ResponseMessages = { proofNotFound: 'Proof presentation not found', invitationNotFound: 'Invitation not found', platformConfigNotFound: 'Platform config not found', + batchEmailSend: 'Unable to send email in batches', emailSend: 'Unable to send email to the user' } }, @@ -352,7 +369,9 @@ export const ResponseMessages = { updateSchemaId: 'Error while updating the schema id', updateCredDefId: 'Error while updating the credential-definition', invalidMessage: 'Invalid transaction details. Missing "message" property.', - invalidTransactionMessage: 'Invalid transaction details' + invalidTransactionMessage: 'Invalid transaction details', + ecosystemRoleNotMatch: 'Ecosystem role not match', + orgEcoIdRequired: 'OrgId & EcosystemId is required' } }, bulkIssuance: { @@ -391,5 +410,20 @@ export const ResponseMessages = { getshorteningUrl:'Shortening Url fetched successfully', createShorteningUrl: 'Shortening Url created successfully' } - } + }, + notification: { + success: { + register: 'Notification webhook registration process completed successfully', + sendNotification: 'Notification sent successfully' + }, + error: { + notFound: 'Notification record not found.', + invalidUrl: 'Invalid URL' + } + }, + storeObject: { + success: { + storeObject: 'Data stored successfully' + } + } }; \ No newline at end of file diff --git a/libs/enum/src/enum.ts b/libs/enum/src/enum.ts index 2dd5470ed..047256879 100644 --- a/libs/enum/src/enum.ts +++ b/libs/enum/src/enum.ts @@ -8,6 +8,25 @@ export enum AgentType { ACAPY = 'ACAPY' } +export declare enum KeyType { + Ed25519 = 'ed25519', + Bls12381g1g2 = 'bls12381g1g2', + Bls12381g1 = 'bls12381g1', + Bls12381g2 = 'bls12381g2', + X25519 = 'x25519', + P256 = 'p256', + P384 = 'p384', + P521 = 'p521', + K256 = 'k256' +} + +export enum DidMethod { + INDY = 'indy', + KEY = 'key', + WEB = 'web', + POLYGON = 'polygon' +} + export enum Ledgers { Bcovrin_Testnet = 'Bcovrin Testnet', Indicio_Testnet = 'Indicio Testnet', @@ -70,10 +89,17 @@ export enum AutoAccept { Never = "never" } +export enum SortMembers { + CREATED_DATE_TIME = 'createDateTime', + STATUS = 'status', + ID = 'id', + ORGANIZATION = 'organization' +} + const transitionMap: { [key in Invitation]: Invitation[] } = { [Invitation.PENDING]: [Invitation.ACCEPTED, Invitation.REJECTED], [Invitation.ACCEPTED]: [], [Invitation.REJECTED]: [] }; -export const transition = (currentStatus: Invitation, nextStatus: Invitation): boolean => (transitionMap[currentStatus].includes(nextStatus)); +export const transition = (currentStatus: Invitation, nextStatus: Invitation): boolean => (transitionMap[currentStatus].includes(nextStatus)); \ No newline at end of file diff --git a/libs/http-exception.filter.ts b/libs/http-exception.filter.ts index 645b20b3a..c7dc008fb 100644 --- a/libs/http-exception.filter.ts +++ b/libs/http-exception.filter.ts @@ -39,6 +39,10 @@ export class HttpExceptionFilter implements ExceptionFilter { httpStatus = HttpStatus.BAD_REQUEST; message = exception?.response?.message || exception?.message; break; + case 'P2023': // Inconsistent column data: {message} + httpStatus = HttpStatus.BAD_REQUEST; + message = exception?.meta?.message || exception?.message; + break; case 'P2018': // The required connected records were not found. {details} case 'P2025': // An operation failed because it depends on one or more records that were required but not found. {cause} case 'P2015': // A related record could not be found. {details} diff --git a/libs/keycloak-url/src/keycloak-url.service.ts b/libs/keycloak-url/src/keycloak-url.service.ts index cb766e604..d10c9f5f8 100644 --- a/libs/keycloak-url/src/keycloak-url.service.ts +++ b/libs/keycloak-url/src/keycloak-url.service.ts @@ -71,6 +71,48 @@ export class KeycloakUrlService { return `${process.env.KEYCLOAK_DOMAIN}admin/realms/${realm}/clients/${clientid}/client-secret`; } + async GetClientRoleURL( + realm: string, + clientid: string, + roleName = '' + ):Promise { + + if ('' === roleName) { + return `${process.env.KEYCLOAK_DOMAIN}admin/realms/${realm}/clients/${clientid}/roles`; + } + + return `${process.env.KEYCLOAK_DOMAIN}admin/realms/${realm}/clients/${clientid}/roles/${roleName}`; + + } + + async GetRealmRoleURL( + realm: string, + roleName = '' + ):Promise { + + if ('' === roleName) { + return `${process.env.KEYCLOAK_DOMAIN}admin/realms/${realm}/roles`; + } + + return `${process.env.KEYCLOAK_DOMAIN}admin/realms/${realm}/roles/${roleName}`; + + } + + async GetClientUserRoleURL( + realm: string, + userId: string, + clientId?: string + ):Promise { + + if (clientId) { + return `${process.env.KEYCLOAK_DOMAIN}admin/realms/${realm}/users/${userId}/role-mappings/clients/${clientId}`; + } + + return `${process.env.KEYCLOAK_DOMAIN}admin/realms/${realm}/users/${userId}/role-mappings/realm`; + + } + + async GetClientIdpURL( realm: string, idp: string diff --git a/libs/org-roles/interfaces/org-roles.interface.ts b/libs/org-roles/interfaces/org-roles.interface.ts index b170d93af..ec63e63c1 100644 --- a/libs/org-roles/interfaces/org-roles.interface.ts +++ b/libs/org-roles/interfaces/org-roles.interface.ts @@ -2,8 +2,8 @@ export interface IOrgRoles { id: string; name: string; description: string; - createDateTime: Date; - createdBy: string; - lastChangedDateTime: Date; - lastChangedBy: string; + createDateTime?: Date; + createdBy?: string; + lastChangedDateTime?: Date; + lastChangedBy?: string; } \ No newline at end of file diff --git a/libs/prisma-service/prisma/data/credebl-master-table.json b/libs/prisma-service/prisma/data/credebl-master-table.json index 028c6cfd9..94c5c465c 100644 --- a/libs/prisma-service/prisma/data/credebl-master-table.json +++ b/libs/prisma-service/prisma/data/credebl-master-table.json @@ -129,6 +129,16 @@ "registerDIDEndpoint": "https://selfserve.indiciotech.io/nym", "registerDIDPayload": "", "indyNamespace": "indicio:mainnet" + }, + { + "name": "Polygon Testnet", + "networkType": "testnet", + "poolConfig": "", + "isActive": true, + "networkString": "testnet", + "registerDIDEndpoint": "", + "registerDIDPayload": "", + "indyNamespace": "polygon:testnet" } ], "endorseData": [ @@ -186,5 +196,39 @@ "key": "multiEcosystemSupport", "value": "false" } + ], + "ledgerConfig": [ + { + "name": "indy", + "details": { + "bcovrin": { + "testnet":"did:indy:bcovrin:testnet" + }, + "indicio": { + "testnet":"did:indy:indicio:testnet", + "demonet":"did:indy:indicio:demonet", + "mainnet":"did:indy:indicio:mainnet" + } + } + }, + { + "name": "polygon", + "details": { + "mainnet": "did:polygon:mainnet", + "testnet": "did:polygon:testnet" + } + }, + { + "name": "key", + "details": { + "key": "did:key" + } + }, + { + "name": "web", + "details": { + "web": "did:web" + } + } ] } \ No newline at end of file diff --git a/libs/prisma-service/prisma/migrations/20240219142237_notification/migration.sql b/libs/prisma-service/prisma/migrations/20240219142237_notification/migration.sql new file mode 100644 index 000000000..3cbdcc3a5 --- /dev/null +++ b/libs/prisma-service/prisma/migrations/20240219142237_notification/migration.sql @@ -0,0 +1,16 @@ +-- CreateTable +CREATE TABLE "notification" ( + "id" UUID NOT NULL, + "orgId" UUID, + "notificationWebhook" TEXT, + "createDateTime" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "createdBy" TEXT NOT NULL DEFAULT '1', + "lastChangedDateTime" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "lastChangedBy" TEXT NOT NULL DEFAULT '1', + "deletedAt" TIMESTAMP(6), + + CONSTRAINT "notification_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "notification_orgId_key" ON "notification"("orgId"); diff --git a/libs/prisma-service/prisma/migrations/20240220121359_user_org_idproleid/migration.sql b/libs/prisma-service/prisma/migrations/20240220121359_user_org_idproleid/migration.sql new file mode 100644 index 000000000..4d6fed26f --- /dev/null +++ b/libs/prisma-service/prisma/migrations/20240220121359_user_org_idproleid/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "user_org_roles" ADD COLUMN "idpRoleId" UUID; diff --git a/libs/prisma-service/prisma/migrations/20240223140032_ledger_config/migration.sql b/libs/prisma-service/prisma/migrations/20240223140032_ledger_config/migration.sql new file mode 100644 index 000000000..ed5a71d72 --- /dev/null +++ b/libs/prisma-service/prisma/migrations/20240223140032_ledger_config/migration.sql @@ -0,0 +1,13 @@ +-- CreateTable +CREATE TABLE "ledgerConfig" ( + "id" UUID NOT NULL, + "name" TEXT NOT NULL, + "details" JSONB NOT NULL, + "createDateTime" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "createdBy" TEXT NOT NULL DEFAULT '1', + "lastChangedDateTime" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "lastChangedBy" TEXT NOT NULL DEFAULT '1', + "deletedAt" TIMESTAMP(6), + + CONSTRAINT "ledgerConfig_pkey" PRIMARY KEY ("id") +); diff --git a/libs/prisma-service/prisma/migrations/20240312064123_agent_invitation_add_recipient_key/migration.sql b/libs/prisma-service/prisma/migrations/20240312064123_agent_invitation_add_recipient_key/migration.sql new file mode 100644 index 000000000..e74b69d59 --- /dev/null +++ b/libs/prisma-service/prisma/migrations/20240312064123_agent_invitation_add_recipient_key/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "agent_invitations" ADD COLUMN "recipientKey" TEXT; diff --git a/libs/prisma-service/prisma/migrations/20240315121444_org_agents_did_document/migration.sql b/libs/prisma-service/prisma/migrations/20240315121444_org_agents_did_document/migration.sql new file mode 100644 index 000000000..9c0b9a251 --- /dev/null +++ b/libs/prisma-service/prisma/migrations/20240315121444_org_agents_did_document/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "org_agents" ADD COLUMN "didDocument" JSONB; diff --git a/libs/prisma-service/prisma/schema.prisma b/libs/prisma-service/prisma/schema.prisma index 8353b7b46..9e55d94b3 100644 --- a/libs/prisma-service/prisma/schema.prisma +++ b/libs/prisma-service/prisma/schema.prisma @@ -84,6 +84,7 @@ model user_org_roles { userId String @db.Uuid orgRoleId String @db.Uuid orgId String? @db.Uuid + idpRoleId String? @db.Uuid organisation organisation? @relation(fields: [orgId], references: [id]) orgRole org_roles @relation(fields: [orgRoleId], references: [id]) user user @relation(fields: [userId], references: [id]) @@ -185,6 +186,7 @@ model org_agents { orgId String? @unique @db.Uuid orgAgentTypeId String? @db.Uuid ledgerId String? @db.Uuid + didDocument Json? agent_invitations agent_invitations[] agents agents? @relation(fields: [agentId], references: [id]) agents_type agents_type? @relation(fields: [agentsTypeId], references: [id]) @@ -292,6 +294,7 @@ model agent_invitations { lastChangedBy Int @default(1) org_agents org_agents @relation(fields: [agentId], references: [id]) organisation organisation @relation(fields: [orgId], references: [id]) + recipientKey String? } model connections { @@ -474,3 +477,24 @@ model file_data { status Boolean @default(false) credential_data Json? } + +model notification { + id String @id @default(uuid()) @db.Uuid + orgId String? @unique @db.Uuid + notificationWebhook String? + createDateTime DateTime @default(now()) @db.Timestamptz(6) + createdBy String @default("1") + lastChangedDateTime DateTime @default(now()) @db.Timestamptz(6) + lastChangedBy String @default("1") + deletedAt DateTime? @db.Timestamp(6) +} +model ledgerConfig { + id String @id @default(uuid()) @db.Uuid + name String + details Json + createDateTime DateTime @default(now()) @db.Timestamptz(6) + createdBy String @default("1") + lastChangedDateTime DateTime @default(now()) @db.Timestamptz(6) + lastChangedBy String @default("1") + deletedAt DateTime? @db.Timestamp(6) +} \ No newline at end of file diff --git a/libs/prisma-service/prisma/seed.ts b/libs/prisma-service/prisma/seed.ts index 7c2eea86c..fc4bf8814 100644 --- a/libs/prisma-service/prisma/seed.ts +++ b/libs/prisma-service/prisma/seed.ts @@ -13,7 +13,7 @@ const createPlatformConfig = async (): Promise => { try { const existPlatformAdmin = await prisma.platform_config.findMany(); - if (existPlatformAdmin.length === 0) { + if (0 === existPlatformAdmin.length) { const { platformConfigData } = JSON.parse(configData); const platformConfig = await prisma.platform_config.create({ data: platformConfigData @@ -38,9 +38,9 @@ const createOrgRoles = async (): Promise => { in: roleNames } } - }) + }); - if (existOrgRole.length === 0) { + if (0 === existOrgRole.length) { const orgRoles = await prisma.org_roles.createMany({ data: orgRoleData }); @@ -66,9 +66,9 @@ const createAgentTypes = async (): Promise => { in: agentType } } - }) + }); - if (existAgentType.length === 0) { + if (0 === existAgentType.length) { const agentTypes = await prisma.agents_type.createMany({ data: agentTypeData }); @@ -94,9 +94,9 @@ const createOrgAgentTypes = async (): Promise => { in: orgAgentType } } - }) + }); - if (existAgentType.length === 0) { + if (0 === existAgentType.length) { const orgAgentTypes = await prisma.org_agents_type.createMany({ data: orgAgentTypeData }); @@ -122,9 +122,9 @@ const createPlatformUser = async (): Promise => { where: { email: platformAdminData.email } - }) + }); - if (existPlatformAdminUser.length === 0) { + if (0 === existPlatformAdminUser.length) { const platformUser = await prisma.user.create({ data: platformAdminData }); @@ -152,9 +152,9 @@ const createPlatformOrganization = async (): Promise => { where: { name: platformAdminOrganizationData.name } - }) + }); - if (existPlatformAdminUser.length === 0) { + if (0 === existPlatformAdminUser.length) { const platformOrganization = await prisma.organisation.create({ data: platformAdminOrganizationData }); @@ -220,9 +220,9 @@ const createLedger = async (): Promise => { in: ledgerIndyNamespace } } - }) + }); - if (existLedgerIndyNameSpace.length === 0) { + if (0 === existLedgerIndyNameSpace.length) { const createLedger = await prisma.ledgers.createMany({ data: ledgerData @@ -250,9 +250,9 @@ const createEcosystemRoles = async (): Promise => { in: ecosystemRoleDetails } } - }) + }); - if (existEcosystemRole.length === 0) { + if (0 === existEcosystemRole.length) { const ecosystemRoles = await prisma.ecosystem_roles.createMany({ data: ecosystemRoleData }); @@ -279,10 +279,10 @@ const createEcosystemConfig = async (): Promise => { in: ecosystemConfigKey } } - }) + }); - if (existEcosystemConfig.length === 0) { + if (0 === existEcosystemConfig.length) { const configDetails = await prisma.ecosystem_config.createMany({ data: ecosystemConfigData }); @@ -298,6 +298,30 @@ const createEcosystemConfig = async (): Promise => { } }; +const createLedgerConfig = async (): Promise => { + try { + const { ledgerConfig } = JSON.parse(configData); + + const ledgerConfigList = await prisma.ledgerConfig.findMany(); + + + if (0 === ledgerConfigList.length && ledgerConfig) { + const configDetails = await prisma.ledgerConfig.createMany({ + data: ledgerConfig + }); + + logger.log(configDetails); + } else { + logger.log('Already seeding in ledger config'); + } + + + } catch (e) { + logger.error('An error occurred ecosystem config:', e); + } +}; + + async function main(): Promise { await createPlatformConfig(); @@ -310,7 +334,7 @@ async function main(): Promise { await createLedger(); await createEcosystemRoles(); await createEcosystemConfig(); - + await createLedgerConfig(); } @@ -322,4 +346,4 @@ main() logger.error(`In prisma seed initialize`, e); await prisma.$disconnect(); process.exit(1); - }); + }); \ No newline at end of file diff --git a/libs/user-org-roles/repositories/index.ts b/libs/user-org-roles/repositories/index.ts index 618c6d50d..5458796cd 100644 --- a/libs/user-org-roles/repositories/index.ts +++ b/libs/user-org-roles/repositories/index.ts @@ -18,13 +18,14 @@ export class UserOrgRolesRepository { * @returns user details */ // eslint-disable-next-line camelcase - async createUserOrgRole(userId: string, roleId: string, orgId?: string): Promise { + async createUserOrgRole(userId: string, roleId: string, orgId?: string, idpRoleId?: string): Promise { try { const data: { orgRole: { connect: { id: string } }; user: { connect: { id: string } }; organisation?: { connect: { id: string } }; + idpRoleId?: string } = { orgRole: { connect: { id: roleId } }, user: { connect: { id: userId } } @@ -34,6 +35,10 @@ export class UserOrgRolesRepository { data.organisation = { connect: { id: orgId } }; } + if (idpRoleId) { + data.idpRoleId = idpRoleId; + } + const saveResponse = await this.prisma.user_org_roles.create({ data }); diff --git a/libs/user-org-roles/src/user-org-roles.service.ts b/libs/user-org-roles/src/user-org-roles.service.ts index e5677dc19..0b87cd9cf 100644 --- a/libs/user-org-roles/src/user-org-roles.service.ts +++ b/libs/user-org-roles/src/user-org-roles.service.ts @@ -13,8 +13,8 @@ export class UserOrgRolesService { * @returns user details */ // eslint-disable-next-line camelcase - async createUserOrgRole(userId: string, roleId: string, orgId?: string): Promise { - return this.userOrgRoleRepository.createUserOrgRole(userId, roleId, orgId); + async createUserOrgRole(userId: string, roleId: string, orgId?: string, idpRoleId?: string): Promise { + return this.userOrgRoleRepository.createUserOrgRole(userId, roleId, orgId, idpRoleId); } @@ -46,10 +46,14 @@ export class UserOrgRolesService { * @param roleIds * @returns */ - async updateUserOrgRole(userId: string, orgId: string, roleIds: string[]): Promise { + async updateUserOrgRole( + userId: string, + orgId: string, + roleIdList: {roleId: string, idpRoleId: string}[] + ): Promise { - for (const role of roleIds) { - this.userOrgRoleRepository.createUserOrgRole(userId, role, orgId); + for (const roleData of roleIdList) { + this.userOrgRoleRepository.createUserOrgRole(userId, roleData.roleId, orgId, roleData.idpRoleId); } return true; diff --git a/nest-cli.json b/nest-cli.json index 18ed21e79..76d477731 100644 --- a/nest-cli.json +++ b/nest-cli.json @@ -277,6 +277,15 @@ "compilerOptions": { "tsConfigPath": "apps/utility/tsconfig.app.json" } + }, + "notification": { + "type": "application", + "root": "apps/notification", + "entryFile": "main", + "sourceRoot": "apps/notification/src", + "compilerOptions": { + "tsConfigPath": "apps/notification/tsconfig.app.json" + } } } } \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5fe5087c8..8edc999d0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ dependencies: '@nestjs/bull': specifier: ^10.0.1 version: 10.0.1(@nestjs/common@10.2.8)(@nestjs/core@10.1.3)(bull@4.11.4) + '@nestjs/cache-manager': + specifier: ^2.1.0 + version: 2.2.1(@nestjs/common@10.2.8)(@nestjs/core@10.1.3)(cache-manager@5.4.0)(rxjs@7.8.1) '@nestjs/common': specifier: ^10.2.7 version: 10.2.8(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) @@ -25,7 +28,7 @@ dependencies: version: 10.1.0(@nestjs/common@10.2.8) '@nestjs/microservices': specifier: ^10.1.3 - version: 10.1.3(@nestjs/common@10.2.8)(@nestjs/core@10.1.3)(@nestjs/websockets@10.1.3)(nats@2.15.1)(reflect-metadata@0.1.13)(rxjs@7.8.1) + version: 10.1.3(@nestjs/common@10.2.8)(@nestjs/core@10.1.3)(@nestjs/websockets@10.1.3)(cache-manager@5.4.0)(nats@2.15.1)(reflect-metadata@0.1.13)(rxjs@7.8.1) '@nestjs/passport': specifier: ^10.0.0 version: 10.0.0(@nestjs/common@10.2.8)(passport@0.6.0) @@ -75,8 +78,8 @@ dependencies: specifier: ^9.22.1 version: 9.22.1 aws-sdk: - specifier: ^2.1492.0 - version: 2.1499.0 + specifier: ^2.1510.0 + version: 2.1560.0 bcrypt: specifier: ^5.1.0 version: 5.1.0 @@ -92,6 +95,12 @@ dependencies: bull: specifier: ^4.11.4 version: 4.11.4 + cache-manager: + specifier: ^5.2.4 + version: 5.4.0 + cache-manager-redis-store: + specifier: ^2.0.0 + version: 2.0.0 class-transformer: specifier: ^0.5.1 version: 0.5.1 @@ -1208,6 +1217,20 @@ packages: tslib: 2.6.0 dev: false + /@nestjs/cache-manager@2.2.1(@nestjs/common@10.2.8)(@nestjs/core@10.1.3)(cache-manager@5.4.0)(rxjs@7.8.1): + resolution: {integrity: sha512-mXj0zenuyMPJICokwVud4Kjh0+pzBNBAgfpx3I48LozNkd8Qfv/MAhZsb15GihGpbFRxafUo3p6XvtAqRm8GRw==} + peerDependencies: + '@nestjs/common': ^9.0.0 || ^10.0.0 + '@nestjs/core': ^9.0.0 || ^10.0.0 + cache-manager: <=5 + rxjs: ^7.0.0 + dependencies: + '@nestjs/common': 10.2.8(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/core': 10.1.3(@nestjs/common@10.2.8)(@nestjs/microservices@10.1.3)(@nestjs/platform-express@10.1.3)(@nestjs/websockets@10.1.3)(reflect-metadata@0.1.13)(rxjs@7.8.1) + cache-manager: 5.4.0 + rxjs: 7.8.1 + dev: false + /@nestjs/cli@10.1.11: resolution: {integrity: sha512-ORkpVFQvcPYtvkLfa0I9dMSPIppkqTOyLqPvJV0wiZofp8iR1+VEVzJVi+PMj53gOkly8TV9+6iy/dBA5Ssrog==} engines: {node: '>= 16'} @@ -1303,7 +1326,7 @@ packages: optional: true dependencies: '@nestjs/common': 10.2.8(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) - '@nestjs/microservices': 10.1.3(@nestjs/common@10.2.8)(@nestjs/core@10.1.3)(@nestjs/websockets@10.1.3)(nats@2.15.1)(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/microservices': 10.1.3(@nestjs/common@10.2.8)(@nestjs/core@10.1.3)(@nestjs/websockets@10.1.3)(cache-manager@5.4.0)(nats@2.15.1)(reflect-metadata@0.1.13)(rxjs@7.8.1) '@nestjs/platform-express': 10.1.3(@nestjs/common@10.2.8)(@nestjs/core@10.1.3) '@nestjs/websockets': 10.1.3(@nestjs/common@10.2.8)(@nestjs/core@10.1.3)(@nestjs/platform-socket.io@10.1.3)(reflect-metadata@0.1.13)(rxjs@7.8.1) '@nuxtjs/opencollective': 0.3.2 @@ -1346,7 +1369,7 @@ packages: reflect-metadata: 0.1.13 dev: false - /@nestjs/microservices@10.1.3(@nestjs/common@10.2.8)(@nestjs/core@10.1.3)(@nestjs/websockets@10.1.3)(nats@2.15.1)(reflect-metadata@0.1.13)(rxjs@7.8.1): + /@nestjs/microservices@10.1.3(@nestjs/common@10.2.8)(@nestjs/core@10.1.3)(@nestjs/websockets@10.1.3)(cache-manager@5.4.0)(nats@2.15.1)(reflect-metadata@0.1.13)(rxjs@7.8.1): resolution: {integrity: sha512-IBKefw+DR6v2SaXjPJ8tRT+gQTJUSGN83gxuaA32uCQNW2rK+CyVapgX3fDeM/zJsLfBkdveSMX+R74w5wuk+Q==} peerDependencies: '@grpc/grpc-js': '*' @@ -1385,6 +1408,7 @@ packages: '@nestjs/common': 10.2.8(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) '@nestjs/core': 10.1.3(@nestjs/common@10.2.8)(@nestjs/microservices@10.1.3)(@nestjs/platform-express@10.1.3)(@nestjs/websockets@10.1.3)(reflect-metadata@0.1.13)(rxjs@7.8.1) '@nestjs/websockets': 10.1.3(@nestjs/common@10.2.8)(@nestjs/core@10.1.3)(@nestjs/platform-socket.io@10.1.3)(reflect-metadata@0.1.13)(rxjs@7.8.1) + cache-manager: 5.4.0 iterare: 1.2.1 nats: 2.15.1 reflect-metadata: 0.1.13 @@ -1507,7 +1531,7 @@ packages: dependencies: '@nestjs/common': 10.2.8(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) '@nestjs/core': 10.1.3(@nestjs/common@10.2.8)(@nestjs/microservices@10.1.3)(@nestjs/platform-express@10.1.3)(@nestjs/websockets@10.1.3)(reflect-metadata@0.1.13)(rxjs@7.8.1) - '@nestjs/microservices': 10.1.3(@nestjs/common@10.2.8)(@nestjs/core@10.1.3)(@nestjs/websockets@10.1.3)(nats@2.15.1)(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/microservices': 10.1.3(@nestjs/common@10.2.8)(@nestjs/core@10.1.3)(@nestjs/websockets@10.1.3)(cache-manager@5.4.0)(nats@2.15.1)(reflect-metadata@0.1.13)(rxjs@7.8.1) '@nestjs/platform-express': 10.1.3(@nestjs/common@10.2.8)(@nestjs/core@10.1.3) tslib: 2.6.1 dev: true @@ -2672,8 +2696,8 @@ packages: resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==} engines: {node: '>= 0.4'} - /aws-sdk@2.1499.0: - resolution: {integrity: sha512-kh89lcXx7lP83uVjzRPkOueRoM8gQlep86W9+l3qCTHSLiVJuc0MiPmqCLMPlOAZil+35roFkwWIP2FJ1WcdXg==} + /aws-sdk@2.1560.0: + resolution: {integrity: sha512-nakTZHytnhKWZpwu9d1crqjoegBRG+j1/rflsVnckXxoIwlKM0D/v/NIe+BJmRnCA2aCdwuMx3dtkgLz/AB6VA==} engines: {node: '>= 10.0.0'} dependencies: buffer: 4.9.2 @@ -2685,7 +2709,7 @@ packages: url: 0.10.3 util: 0.12.5 uuid: 8.0.0 - xml2js: 0.5.0 + xml2js: 0.6.2 dev: false /aws-sign2@0.7.0: @@ -3051,6 +3075,20 @@ packages: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} + /cache-manager-redis-store@2.0.0: + resolution: {integrity: sha512-bWLWlUg6nCYHiJLCCYxY2MgvwvKnvlWwrbuynrzpjEIhfArD2GC9LtutIHFEPeyGVQN6C+WEw+P3r+BFBwhswg==} + engines: {node: '>= 8.3'} + dependencies: + redis: 3.1.2 + dev: false + + /cache-manager@5.4.0: + resolution: {integrity: sha512-FS7o8vqJosnLpu9rh2gQTo8EOzCRJLF1BJ4XDEUDMqcfvs7SJZs5iuoFTXLauzQ3S5v8sBAST1pCwMaurpyi1A==} + dependencies: + lodash.clonedeep: 4.5.0 + lru-cache: 10.2.0 + promise-coalesce: 1.1.2 + /call-bind@1.0.2: resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==} dependencies: @@ -3643,6 +3681,11 @@ packages: resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} dev: false + /denque@1.5.1: + resolution: {integrity: sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw==} + engines: {node: '>=0.10'} + dev: false + /denque@2.1.0: resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} engines: {node: '>=0.10'} @@ -6286,7 +6329,6 @@ packages: /lodash.clonedeep@4.5.0: resolution: {integrity: sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==} - dev: false /lodash.defaults@4.2.0: resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} @@ -6332,6 +6374,11 @@ packages: /lru-cache@10.0.1: resolution: {integrity: sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==} engines: {node: 14 || >=16.14} + dev: true + + /lru-cache@10.2.0: + resolution: {integrity: sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==} + engines: {node: 14 || >=16.14} /lru-cache@4.0.2: resolution: {integrity: sha512-uQw9OqphAGiZhkuPlpFGmdTU2tEuhxTourM/19qGJrxBPHAr/f8BT1a0i/lOclESnGatdJG/UCkP9kZB/Lh1iw==} @@ -7320,6 +7367,10 @@ packages: engines: {node: '>=0.4.0'} dev: false + /promise-coalesce@1.1.2: + resolution: {integrity: sha512-zLaJ9b8hnC564fnJH6NFSOGZYYdzrAJn2JUUIwzoQb32fG2QAakpDNM+CZo1km6keXkRXRM+hml1BFAPVnPkxg==} + engines: {node: '>=16'} + /prompts@2.4.2: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} @@ -7584,6 +7635,10 @@ packages: resolve: 1.22.2 dev: true + /redis-commands@1.7.0: + resolution: {integrity: sha512-nJWqw3bTFy21hX/CPKHth6sfhZbdiHP6bTawSgQBlKOVRG7EZkfHbbHwQJnrE4vsQf0CMNE+3gJ4Fmm16vdVlQ==} + dev: false + /redis-errors@1.2.0: resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} engines: {node: '>=4'} @@ -7596,6 +7651,16 @@ packages: redis-errors: 1.2.0 dev: false + /redis@3.1.2: + resolution: {integrity: sha512-grn5KoZLr/qrRQVwoSkmzdbw6pwF+/rwODtrOr6vuBRiR/f3rjSTGupbF90Zpqm2oenix8Do6RV7pYEkGwlKkw==} + engines: {node: '>=10'} + dependencies: + denque: 1.5.1 + redis-commands: 1.7.0 + redis-errors: 1.2.0 + redis-parser: 3.0.0 + dev: false + /reflect-metadata@0.1.13: resolution: {integrity: sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==} @@ -9311,8 +9376,8 @@ packages: xmlbuilder: 11.0.1 dev: false - /xml2js@0.5.0: - resolution: {integrity: sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==} + /xml2js@0.6.2: + resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==} engines: {node: '>=4.0.0'} dependencies: sax: 1.2.4