diff --git a/.env.sample b/.env.sample new file mode 100644 index 000000000..e86573a16 --- /dev/null +++ b/.env.sample @@ -0,0 +1,111 @@ +MODE=DEV + +API_GATEWAY_PROTOCOL=http +API_GATEWAY_HOST='0.0.0.0' +API_GATEWAY_PORT=5000 + +PLATFORM_NAME=CREDEBL + +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 +S3_BUCKET_ARN=arn:aws:s3:::xxxxx // Please provide your AWS bucket arn + +TENANT_EMAIL_LOGO=credebl.jpg +API_ENDPOINT=localhost:5000 #Use your local machine IP Address & PORT +API_ENDPOINT_PORT=5000 + +SOCKET_HOST=http://localhost:5000 + +NATS_HOST='0.0.0.0' +NATS_PORT=4222 +NATS_URL=nats://0.0.0.0:4222 + +REDIS_HOST='localhost' # Use IP Address +REDIS_PORT=6379 + +POSTGRES_HOST=localhost # Use IP Address +POSTGRES_PORT=5432 +POSTGRES_USER=postgres +POSTGRES_PASSWORD=xxxxxxxx +POSTGRES_DATABASE=postgres + +POSTGRES_MEDIATOR_DATABASE='mediator_agent' +POSTGRES_MEDIATOR_PORT=5431 + +MEDIATOR_AGENT_LABEL=MediatorAgent +MEDIATOR_AGENT_ENDPOINT='' + +SENDGRID_API_KEY=xxxxxxxxxxxxxx // Please provide your sendgrid API key + +FRONT_END_URL=http://localhost:3000 + +FILE_SERVER=credebl-dev-mediator-indypool +FILE_SERVER_PORT=8081 +FILE_SERVER_USER=credebl +FILE_SERVER_HOST=0.0.0.0 + +REMOTE_FILE_DIR='/opt/cb-tails-file-server/tails/tails-files/' +ACCESSIBLE_FILE_LOCATION='tails-files' + +LOCAL_FILE_SERVER=/opt/credebl-platform/tails-files/ +GCLOUD_ENGINE_PATH=/home/credebl/.ssh/google_compute_engine + +AFJ_AGENT_SPIN_UP=/apps/agent-provisioning/AFJ/scripts/start_agent.sh + +AGENT_SPIN_UP_FILE=/agent-spinup/scripts/start_agent.sh +LIBINDY_KEY=CE7709D068DB5E88 + +AGENT_RESTART_SCRIPT=/agent-spinup/scripts/manage_agent.sh +AGENT_STATUS_SCRIPT=/agent-spinup/scripts/status_agent.sh + +WALLET_PROVISION_SCRIPT=/agent-spinup/scripts/wallet_provision.sh +WALLET_STORAGE_HOST=localhost # Use IP Address +WALLET_STORAGE_PORT=5432 +WALLET_STORAGE_USER=postgres +WALLET_STORAGE_PASSWORD=xxxxxx + +KEYCLOAK_DOMAIN=http://localhost:8089/auth/ +KEYCLOAK_ADMIN_URL=http://localhost:8089 +KEYCLOAK_MASTER_REALM=master +KEYCLOAK_CREDEBL_REALM=credebl-platform +KEYCLOAK_MANAGEMENT_CLIENT_ID=adminClient +KEYCLOAK_MANAGEMENT_CLIENT_SECRET=xxxxxx-xxxx-xxxx-xxxx-xxxxxx #Refer from ADMIN CONSOLE of your Keycloak +KEYCLOAK_MANAGEMENT_ADEYA_CLIENT_ID=adeyaClient +KEYCLOAK_MANAGEMENT_ADEYA_CLIENT_SECRET=xxxxxx-xxxx-xxxx-xxxx-xxxxxx #Refer from ADMIN CONSOLE of your Keycloak + +FILE_UPLOAD_PATH_TENANT= /uploadedFiles/tenant-logo/ + +CRYPTO_PRIVATE_KEY=xxxxx-xxxxx-xxxxx-xxxxx +PLATFORM_URL=https://dev.credebl.com +KEYCLOAK_URL=http://localhost:8089 +PLATFORM_PROFILE_MODE=DEV + +AFJ_VERSION=afj-0.4.0:latest +INVOICE_PDF_URL=./invoice-pdf + +FIDO_API_ENDPOINT=http://localhost:8000 # Host:port of your FIDO (WebAuthn) Server + +PLATFORM_WALLET_NAME=platform-admin +PLATFORM_WALLET_PASSWORD= // Please provide encrypt password using crypto-js +PLATFORM_SEED= // The seed should consist of 32 characters. +PLATFORM_ID= + +AFJ_AGENT_ENDPOINT_PATH=/apps/agent-provisioning/AFJ/endpoints/ + +# This was inserted by prisma init: +# Environment variables declared in this file are automatically made available to Prisma. +# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema + +# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB (Preview). +# See the documentation for all the connection string options: https://pris.ly/d/connection-strings + +DATABASE_URL="postgresql://postgres:xxxxxx@localhost:5432/postgres?schema=public" #Use the correct user/pwd, IP Address + +# enable only prisma:engine-level debugging output +export DEBUG="prisma:engine" + +# enable only prisma:client-level debugging output +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 diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 000000000..e69de29bb diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 000000000..f55a1efc2 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,104 @@ +module.exports = { + parser: '@typescript-eslint/parser', + parserOptions: { + project: 'tsconfig.json', + sourceType: 'module' + }, + plugins: ['@typescript-eslint/eslint-plugin'], + extends: ['plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended'], + root: true, + env: { + node: true, + jest: true + }, + ignorePatterns: ['.eslintrc.js'], + rules: { + 'prettier/prettier': 0, + 'no-console': 'error', + // "@typescript-eslint/consistent-type-imports": "error", + '@typescript-eslint/no-unused-vars': [ + 'error' + // { + // "argsIgnorePattern": "_" + // } + ], + '@typescript-eslint/array-type': 'error', + 'template-curly-spacing': 'error', + '@typescript-eslint/explicit-function-return-type': 'error', + '@typescript-eslint/explicit-module-boundary-types': 'warn', + '@typescript-eslint/no-explicit-any': 'error', + '@typescript-eslint/no-use-before-define': 'error', + complexity: ['error', 50], + 'array-callback-return': 'error', + curly: 'error', + 'default-case': 'error', + 'default-case-last': 'error', + 'default-param-last': 'error', + camelcase: [2, { properties: 'always' }], + 'no-invalid-this': 'error', + 'no-return-assign': 'error', + 'no-unused-expressions': ['error', { allowTernary: true }], + 'no-useless-concat': 'error', + 'no-useless-return': 'error', + 'guard-for-in': 'error', + 'no-case-declarations': 'error', + 'no-implicit-coercion': 'error', + 'no-lone-blocks': 'error', + 'no-loop-func': 'error', + 'no-param-reassign': 'error', + 'no-return-await': 'error', + 'no-self-compare': 'error', + 'no-throw-literal': 'error', + 'no-useless-catch': 'error', + 'prefer-promise-reject-errors': 'error', + 'vars-on-top': 'error', + yoda: ['error', 'always'], + 'arrow-body-style': ['warn', 'as-needed'], + 'no-useless-rename': 'error', + 'prefer-destructuring': [ + 'error', + { + array: true, + object: true + }, + { + enforceForRenamedProperties: false + } + ], + 'prefer-numeric-literals': 'error', + 'prefer-rest-params': 'warn', + 'prefer-spread': 'error', + 'array-bracket-newline': ['error', { multiline: true, minItems: null }], + 'array-bracket-spacing': 'error', + 'brace-style': ['error', '1tbs', { allowSingleLine: true }], + 'block-spacing': 'error', + 'comma-dangle': 'error', + 'comma-spacing': 'error', + 'comma-style': 'error', + 'computed-property-spacing': 'error', + 'func-call-spacing': 'error', + 'implicit-arrow-linebreak': ['error', 'beside'], + 'keyword-spacing': 'error', + 'multiline-ternary': ['error', 'always-multiline'], + 'no-mixed-operators': 'error', + 'no-multiple-empty-lines': ['error', { max: 2, maxEOF: 1 }], + 'no-tabs': 'error', + 'no-unneeded-ternary': 'error', + 'no-whitespace-before-property': 'error', + 'nonblock-statement-body-position': ['error', 'below'], + 'object-property-newline': ['error', { allowAllPropertiesOnSameLine: true }], + semi: ['error', 'always'], + 'semi-spacing': 'error', + 'space-before-blocks': 'error', + 'space-in-parens': 'error', + 'space-infix-ops': 'error', + 'space-unary-ops': 'error', + 'arrow-spacing': 'error', + 'no-confusing-arrow': 'off', + 'no-var': 'error', + 'object-shorthand': 'error', + 'prefer-const': 'error', + 'prefer-template': 'error', + quotes: ['warn', 'single', { allowTemplateLiterals: true }] + } +}; \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..08cd90e3e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,31 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Prerequisites** + +**Steps to Reproduce** + +**Current behavior** + +**Expected behavior** + +**Environment** + +**Desktop** + - OS: + - Browser: + - Browser Version: + - CREDEBL Version: + +**Smartphone** + - Device: + - OS: + - ADEYA version: + +**Screenshots or Screen recording** diff --git a/.gitignore b/.gitignore index c6bba5913..4281aedfc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,130 +1,8 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -lerna-debug.log* -.pnpm-debug.log* - -# Diagnostic reports (https://nodejs.org/api/report.html) -report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json - -# Runtime data -pids -*.pid -*.seed -*.pid.lock - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage -*.lcov - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Bower dependency directory (https://bower.io/) -bower_components - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release - -# Dependency directories -node_modules/ -jspm_packages/ - -# Snowpack dependency directory (https://snowpack.dev/) -web_modules/ - -# TypeScript cache -*.tsbuildinfo - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Optional stylelint cache -.stylelintcache - -# Microbundle cache -.rpt2_cache/ -.rts2_cache_cjs/ -.rts2_cache_es/ -.rts2_cache_umd/ - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variable files -.env -.env.development.local -.env.test.local -.env.production.local -.env.local - -# parcel-bundler cache (https://parceljs.org/) -.cache -.parcel-cache - -# Next.js build output -.next -out - -# Nuxt.js build / generate output -.nuxt +node_modules dist - -# Gatsby files -.cache/ -# Comment in the public line in if your project uses Gatsby and not Next.js -# https://nextjs.org/blog/next-9-1#public-directory-support -# public - -# vuepress build output -.vuepress/dist - -# vuepress v2.x temp and cache directory -.temp -.cache - -# Docusaurus cache and generated files -.docusaurus - -# Serverless directories -.serverless/ - -# FuseBox cache -.fusebox/ - -# DynamoDB Local files -.dynamodb/ - -# TernJS port file -.tern-port - -# Stores VSCode versions used for testing VSCode extensions -.vscode-test - -# yarn v2 -.yarn/cache -.yarn/unplugged -.yarn/build-state.yml -.yarn/install-state.gz -.pnp.* +uploadedFiles +.env +sonar-project.properties +.scannerwork/* +coverage +libs/prisma-service/prisma/data/credebl-master-table.json diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..91d365bab --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "apps/agent-provisioning/AFJ/afj-controller"] + path = apps/agent-provisioning/AFJ/afj-controller + url = https://github.com/credebl/afj-controller.git diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 000000000..d24fdfc60 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/husky.sh" + +npx lint-staged diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 000000000..aacb51810 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +18.17 diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 000000000..d45b4ba45 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,9 @@ +{ + "tabWidth": 2, + "singleQuote": true, + "trailingComma": "none", + "useTabs": false, + "semi": true, + "bracketSpacing":true, + "printWidth":120 +} diff --git a/README-Microservice.md b/README-Microservice.md new file mode 100644 index 000000000..4e6a2c820 --- /dev/null +++ b/README-Microservice.md @@ -0,0 +1,18 @@ +# CREDEBL migration to Microservices + +## Run CREDEBL Micro-services + +```bash +$ npm install +``` +## Creating the individual microservice mode structure as follows: +`nest generate app [my-app]` + +## Starting the individual Microservices +`nest start [my-app] [--watch]` + +## Creating the libraries +`nest g library [my-library]` + +### library schematic prompts you for a prefix (credebl alias) for the library: +`What prefix would you like to use for the library (default: @app)? credebl` \ No newline at end of file diff --git a/README.md b/README.md index 94332c152..88ec9e2c2 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,99 @@ -# credebl-platform -Open source, Open standards based Decentralised Identity & Verifiable Credentials Platform +# CREDEBL SSI Platform + +This repository host codebase for CREDEBL SSI Platform backend. + +## Pre-requisites + +Install Docker and docker-compose +
See: https://docs.docker.com/engine/install/ + +Install Node: >= 18.17.0 +
See: https://nodejs.dev/en/learn/how-to-install-nodejs/ + +**Install NestJS** +```bash +$ npm i @nestjs/cli@latest +``` + +**Setup & run postgres** +Start the postgresql service using the docker: + +```bash +docker run --name some-postgres -p 5432:5432 -e POSTGRES_PASSWORD= -e POSTGRES_USER=credebl -d postgres +``` + +**Run prisma to generate db schema** + +```bash +$ cd ./libs/prisma-servie/prisma +$ npx prisma generate +$ npx prisma db push +``` + +**Seed initial data** + +```bash +$ cd ./libs/prisma-servie +$ npx prisma db seed +``` + +# Install NATS Message Broker +## Pull NATS docker image + +NATS is used for inter-service communication. The only pre-requisite here is to install docker. + +``` +docker pull nats:latest +``` + +## Run NATS using `docker-compose` +The `docker-compose.yml` file is available in the root folder. + +``` +> docker-compose up +``` + + +## Run CREDEBL Micro-services + +```bash +$ npm install +``` + +## Configure environment variables in `.env` before you start the API Gateway + +## Running the API Gateway app +You can optionally use the `--watch` flag during development / testing. + +```bash +$ nest start [--watch] +``` + +## Starting the individual Micro-services + +### e.g. for starting `organization service` micro-service run below command in a separate terminal window + +```bash +$ nest start organization [--watch] +``` + +### Likewise you can start all the micro-services one after another in separate terminal window + +```bash +$ nest start user [--watch] +$ nest start ledger [--watch] +$ nest start connection [--watch] +$ nest start issuance [--watch] +$ nest start verification [--watch] +$ nest start agent-provisioning [--watch] +$ nest start agent-service [--watch] +``` + +## To access micro-service endpoints using the API Gateway. Navigate to + +``` +http://localhost:5000/api +``` + +## License + Apache 2.0 diff --git a/apps/agent-provisioning/AFJ/scripts/start_agent.sh b/apps/agent-provisioning/AFJ/scripts/start_agent.sh new file mode 100755 index 000000000..881ed70d6 --- /dev/null +++ b/apps/agent-provisioning/AFJ/scripts/start_agent.sh @@ -0,0 +1,160 @@ +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} +ADMIN_PORT=$((8000 + $AGENCY)) +INBOUND_PORT=$((9000 + $AGENCY)) +CONTROLLER_PORT=$((3000 + $AGENCY)) +POSTGRES_PORT=$((5432 + $AGENCY)) +NETWORK_NAME="credebl-network" + +echo "AGENT SPIN-UP STARTED" +if [ ${AGENCY} -eq 1 ]; then + echo "CREATING DOCKER NETWORK" + docker network create --driver bridge --subnet 10.20.0.0/16 --gateway 10.20.0.1 credebl-network + [ $? != 0 ] && error "Failed to create docker network !" && exit 102 + echo "CREATED DOCKER NETWORK. SETTING UP INITIAL IPs" + INTERNAL_IP=10.20.0.2 + POSTGRES_IP=10.20.0.3 + CONTROLLER_IP=10.20.0.4 +fi + +if [ -d "${PWD}/apps/agent-provisioning/AFJ/endpoints" ]; then + echo "Endpoints directory exists." +else + echo "Error: Endpoints directory does not exists." + mkdir ${PWD}/apps/agent-provisioning/AFJ/endpoints +fi + +docker build . -t $AFJ_VERSION -f apps/agent-provisioning/AFJ/afj-controller/Dockerfile + +AGENT_ENDPOINT="${PROTOCOL}://${EXTERNAL_IP}:${INBOUND_PORT}" + +echo "-----$AGENT_ENDPOINT----" +cat <>${PWD}/apps/agent-provisioning/AFJ/agent-config/${AGENCY}_${CONTAINER_NAME}.json +{ + "label": "${AGENCY}_${CONTAINER_NAME}", + "walletId": "$WALLET_NAME", + "walletKey": "$WALLET_PASSWORD", + "walletType": "postgres_storage", + "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", + "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" +cat <>${PWD}/apps/agent-provisioning/AFJ/${FILE_NAME} +version: '3' + +services: + agent: + image: $AFJ_VERSION + + container_name: agent${AGENCY}_${CONTAINER_NAME} + restart: always + environment: + AFJ_REST_LOG_LEVEL: 1 + ports: + - ${INBOUND_PORT}:${INBOUND_PORT} + - ${ADMIN_PORT}:${ADMIN_PORT} + + volumes: + - ./agent-config/${AGENCY}_${CONTAINER_NAME}.json:/config.json + + command: --auto-accept-connections --config /config.json + +volumes: + pgdata: + agent-indy_client: + agent-tmp: +EOF + +if [ $? -eq 0 ]; then + cd apps/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 --project-name agent${AGENCY}_${CONTAINER_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" + cat <>${PWD}/endpoints/${AGENCY}_${CONTAINER_NAME}.json + { + "CONTROLLER_ENDPOINT":"${EXTERNAL_IP}:${ADMIN_PORT}", + "CONTROLLER_IP" : "${CONTROLLER_IP}", + "CONTROLLER_PORT" : "${CONTROLLER_PORT}", + "POSTGRES_ENDPOINT" : "${POSTGRES_IP}:${POSTGRES_PORT}", + "AGENT_ENDPOINT" : "${INTERNAL_IP}:${ADMIN_PORT}" + } +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)" diff --git a/apps/agent-provisioning/AFJ/scripts/start_agent_ecs.sh b/apps/agent-provisioning/AFJ/scripts/start_agent_ecs.sh new file mode 100644 index 000000000..9852cfc99 --- /dev/null +++ b/apps/agent-provisioning/AFJ/scripts/start_agent_ecs.sh @@ -0,0 +1,209 @@ +#!/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} +AGENT_HOST=${15} +AWS_ACCOUNT_ID=${16} +S3_BUCKET_ARN=${17} +ADMIN_PORT=$((8000 + AGENCY)) +INBOUND_PORT=$((9000 + AGENCY)) +CONTROLLER_PORT=$((3000 + AGENCY)) +POSTGRES_PORT=$((5432 + AGENCY)) + +SERVICE_NAME="${CONTAINER_NAME}-service" +CLUSTER_NAME='agent-spinup' +DESIRED_COUNT=1 + +EXTERNAL_IP=$(echo "$2" | tr -d '[:space:]') + +AGENT_ENDPOINT="${PROTOCOL}://${EXTERNAL_IP}:${INBOUND_PORT}" +echo "AGENT SPIN-UP STARTED" + +cat <>/app/agent-provisioning/AFJ/agent-config/${AGENCY}_${CONTAINER_NAME}.json +{ + "label": "${AGENCY}_${CONTAINER_NAME}", + "walletId": "$WALLET_NAME", + "walletKey": "$WALLET_PASSWORD", + "walletType": "postgres_storage", + "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", + "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 +scp ${PWD}/agent-provisioning/AFJ/agent-config/${AGENCY}_${CONTAINER_NAME}.json ${AGENT_HOST}:/home/ec2-user/config/ + +# Construct the container definitions dynamically +CONTAINER_DEFINITIONS=$( + cat <task_definition.json + +# Register the task definition and retrieve the ARN +TASK_DEFINITION_ARN=$(aws ecs register-task-definition --cli-input-json file://task_definition.json --query 'taskDefinition.taskDefinitionArn' --output text) + +# Create the service +aws ecs create-service \ + --cluster $CLUSTER_NAME \ + --service-name $SERVICE_NAME \ + --task-definition $TASK_DEFINITION_ARN \ + --desired-count $DESIRED_COUNT \ + --launch-type EC2 \ + --deployment-configuration "maximumPercent=200,minimumHealthyPercent=100" + +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" + cat <>${PWD}/agent-provisioning/AFJ/endpoints/${AGENCY}_${CONTAINER_NAME}.json + { + "CONTROLLER_ENDPOINT":"${EXTERNAL_IP}:${ADMIN_PORT}", + "CONTROLLER_IP" : "${CONTROLLER_IP}", + "CONTROLLER_PORT" : "${CONTROLLER_PORT}", + "POSTGRES_ENDPOINT" : "${POSTGRES_IP}:${POSTGRES_PORT}", + "AGENT_ENDPOINT" : "${INTERNAL_IP}:${ADMIN_PORT}" + } +EOF + sudo rm -rf ${PWD}/agent-provisioning/AFJ/agent-config/${AGENCY}_${CONTAINER_NAME}.json + echo "Agent config created" +else + echo "===============" + echo "ERROR : Failed to spin up the agent!" + echo "===============" && exit 125 +fi +echo "Total time elapsed: $(date -ud "@$(($(date +%s) - $START_TIME))" +%T) (HH:MM:SS)" diff --git a/apps/agent-provisioning/Dockerfile b/apps/agent-provisioning/Dockerfile new file mode 100644 index 000000000..485a774b0 --- /dev/null +++ b/apps/agent-provisioning/Dockerfile @@ -0,0 +1,53 @@ +# Stage 1: Build the application +FROM node:16-alpine as build + +# Set the working directory +WORKDIR /app + +# Copy package.json and package-lock.json +COPY package.json ./ +#COPY package-lock.json ./ + +# Install dependencies +RUN npm i + +# Copy the rest of the application code +COPY . . +CMD ["sh", "-c", "cd libs/prisma-service && npx prisma generate"] + +# Build the user service +RUN npm run build agent-provisioning + + +# Base image +FROM 668004263903.dkr.ecr.ap-south-1.amazonaws.com/credebl2.0:agent-provisioning-base-image +RUN rm -rf /app/* +# Set the working directory +WORKDIR /app + +RUN mkdir -p ./agent-provisioning/AFJ/endpoints +RUN mkdir -p ./agent-provisioning/AFJ/agent-config + +# Copy the compiled code +COPY --from=build /app/node_modules ./node_modules +COPY --from=build dist/apps/agent-provisioning/ ./dist/apps/agent-provisioning/ +COPY --from=build apps/agent-provisioning/AFJ/scripts ./agent-provisioning/AFJ/scripts + + +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 777 /app/agent-provisioning/AFJ/endpoints +RUN chmod 777 /app/agent-provisioning/AFJ/agent-config + +# Copy the libs folder +COPY libs/ ./libs/ + +# Set the command to run the microservice +CMD ["sh", "-c", "cd libs/prisma-service && npx prisma generate && cd ../.. && node dist/apps/agent-provisioning/main.js"] + + +# Set the command to run the microservice +#CMD ["node", "dist/apps/user/main.js"] +# EXPOSE 5001 +# docker build -t agent-provisioning-service -f apps/agent-provisioning/Dockerfile . +# docker run -d --env-file .env --name agent-provisioning-service agent-provisioning-service \ No newline at end of file diff --git a/apps/agent-provisioning/src/agent-provisioning.controller.ts b/apps/agent-provisioning/src/agent-provisioning.controller.ts new file mode 100644 index 000000000..12f1f6c3b --- /dev/null +++ b/apps/agent-provisioning/src/agent-provisioning.controller.ts @@ -0,0 +1,19 @@ +import { Controller } from '@nestjs/common'; +import { AgentProvisioningService } from './agent-provisioning.service'; +import { MessagePattern } from '@nestjs/microservices'; +import { IWalletProvision } from './interface/agent-provisioning.interfaces'; + +@Controller() +export class AgentProvisioningController { + constructor(private readonly agentProvisioningService: AgentProvisioningService) { } + + /** + * Description: Wallet provision + * @param payload + * @returns Get DID and verkey + */ + @MessagePattern({ cmd: 'wallet-provisioning' }) + walletProvision(payload: IWalletProvision): Promise { + return this.agentProvisioningService.walletProvision(payload); + } +} diff --git a/apps/agent-provisioning/src/agent-provisioning.module.ts b/apps/agent-provisioning/src/agent-provisioning.module.ts new file mode 100644 index 000000000..933b9dcf8 --- /dev/null +++ b/apps/agent-provisioning/src/agent-provisioning.module.ts @@ -0,0 +1,23 @@ +import { Logger, Module } from '@nestjs/common'; +import { AgentProvisioningController } from './agent-provisioning.controller'; +import { AgentProvisioningService } from './agent-provisioning.service'; +import { ClientsModule, Transport } from '@nestjs/microservices'; +import { ConfigModule } from '@nestjs/config'; + +@Module({ + imports: [ + ConfigModule.forRoot(), + ClientsModule.register([ + { + name: 'NATS_CLIENT', + transport: Transport.NATS, + options: { + servers: [`${process.env.NATS_URL}`] + } + } + ]) + ], + controllers: [AgentProvisioningController], + providers: [AgentProvisioningService, Logger] +}) +export class AgentProvisioningModule { } diff --git a/apps/agent-provisioning/src/agent-provisioning.service.ts b/apps/agent-provisioning/src/agent-provisioning.service.ts new file mode 100644 index 000000000..596f3ae93 --- /dev/null +++ b/apps/agent-provisioning/src/agent-provisioning.service.ts @@ -0,0 +1,52 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { RpcException } from '@nestjs/microservices'; +import { IWalletProvision } from './interface/agent-provisioning.interfaces'; +import * as dotenv from 'dotenv'; +import { AgentType } from '@credebl/enum/enum'; +import * as fs from 'fs'; +import { exec } from 'child_process'; +dotenv.config(); + +@Injectable() +export class AgentProvisioningService { + + constructor( + private readonly logger: Logger + ) { } + + /** + * Description: Wallet provision + * @param payload + * @returns Get DID and verkey + */ + async walletProvision(payload: IWalletProvision): Promise { + try { + + const { containerName, externalIp, orgId, seed, walletName, walletPassword, walletStorageHost, walletStoragePassword, walletStoragePort, walletStorageUser, webhookEndpoint, agentType, protocol, afjVersion, tenant } = payload; + + if (agentType === AgentType.AFJ) { + // The wallet provision command is used to invoke a shell script + const walletProvision = `${process.cwd() + process.env.AFJ_AGENT_SPIN_UP + } ${orgId} "${externalIp}" "${walletName}" "${walletPassword}" ${seed} ${webhookEndpoint} ${walletStorageHost} ${walletStoragePort} ${walletStorageUser} ${walletStoragePassword} ${containerName} ${protocol} ${tenant} ${afjVersion} ${process.env.AGENT_HOST} ${process.env.AWS_ACCOUNT_ID} ${process.env.S3_BUCKET_ARN}`; + + const spinUpResponse: object = new Promise(async (resolve) => { + + await exec(walletProvision, async (err, stdout, stderr) => { + this.logger.log(`shell script output: ${stdout}`); + if (stderr) { + this.logger.log(`shell script error: ${stderr}`); + } + const agentEndPoint: string = await fs.readFileSync(`${process.env.PWD}${process.env.AFJ_AGENT_ENDPOINT_PATH}${orgId}_${containerName}.json`, 'utf8'); + resolve(agentEndPoint); + }); + }); + return spinUpResponse; + } else if (agentType === AgentType.ACAPY) { + // TODO: ACA-PY Agent Spin-Up + } + } catch (error) { + this.logger.error(`[walletProvision] - error in wallet provision: ${JSON.stringify(error)}`); + throw new RpcException(error); + } + } +} diff --git a/apps/agent-provisioning/src/interface/agent-provisioning.interfaces.ts b/apps/agent-provisioning/src/interface/agent-provisioning.interfaces.ts new file mode 100644 index 000000000..d74d14090 --- /dev/null +++ b/apps/agent-provisioning/src/interface/agent-provisioning.interfaces.ts @@ -0,0 +1,66 @@ +import { AgentType } from "@credebl/enum/enum"; + +export interface IWalletProvision { + orgId: string; + externalIp: string; + walletName: string; + walletPassword: string; + seed: string; + webhookEndpoint: string; + walletStorageHost: string; + walletStoragePort: string; + walletStorageUser: string; + walletStoragePassword: string; + internalIp: string; + containerName: string; + agentType: AgentType; + orgName: string; + genesisUrl: string; + protocol: string; + afjVersion: string; + tenant: boolean; +} + +export interface IAgentSpinUp { + issuerNumber: string; + issuerName: string; + externalIp: string; + genesisUrl: string; + adminKey: string; + walletName: string; + walletPassword: string; + randomSeed: string; + apiEndpoint: string; + walletStorageHost: string; + walletStoragePort: string; + walletStorageUser: string; + walletStoragePassword: string; + internalIp: string; + tailsFailServer: string; + containerName: string; +} + +export interface IStartStopAgent { + action: string; + orgId: number; + orgName: string; +} + +export interface IAgentStatus { + apiKey: string; + agentEndPoint: string; + orgId: string; + agentSpinUpStatus: number; + orgName: string; +} + +export interface IPlatformConfig { + externalIP: string; + genesisURL: string; + adminKey: string; + lastInternalIP: number; + platformTestNetApiKey: string; + sgEmailFrom: string; + apiEndpoint: string; + tailsFileServer: string; +} diff --git a/apps/agent-provisioning/src/main.ts b/apps/agent-provisioning/src/main.ts new file mode 100644 index 000000000..1b93c0175 --- /dev/null +++ b/apps/agent-provisioning/src/main.ts @@ -0,0 +1,22 @@ +import { NestFactory } from '@nestjs/core'; +import { HttpExceptionFilter } from 'libs/http-exception.filter'; +import { Logger } from '@nestjs/common'; +import { MicroserviceOptions, Transport } from '@nestjs/microservices'; +import { AgentProvisioningModule } from './agent-provisioning.module'; + +const logger = new Logger(); + +async function bootstrap(): Promise { + + const app = await NestFactory.createMicroservice(AgentProvisioningModule, { + transport: Transport.NATS, + options: { + servers: [`${process.env.NATS_URL}`] + } + }); + app.useGlobalFilters(new HttpExceptionFilter()); + + await app.listen(); + logger.log('Agent-Provisioning-Service Microservice is listening to NATS '); +} +bootstrap(); diff --git a/apps/agent-provisioning/test/app.e2e-spec.ts b/apps/agent-provisioning/test/app.e2e-spec.ts new file mode 100644 index 000000000..244d42bd8 --- /dev/null +++ b/apps/agent-provisioning/test/app.e2e-spec.ts @@ -0,0 +1,22 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { INestApplication } from '@nestjs/common'; +import * as request from 'supertest'; +import { AgentProvisioningModule } from './../src/agent-provisioning.module'; + +describe('AgentProvisioningController (e2e)', () => { + let app: INestApplication; + + beforeEach(async () => { + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [AgentProvisioningModule] + }).compile(); + + app = moduleFixture.createNestApplication(); + await app.init(); + }); + + it('/ (GET)', () => request(app.getHttpServer()) + .get('/') + .expect(200) + .expect('Hello World!')); +}); diff --git a/apps/agent-provisioning/test/jest-e2e.json b/apps/agent-provisioning/test/jest-e2e.json new file mode 100644 index 000000000..e9d912f3e --- /dev/null +++ b/apps/agent-provisioning/test/jest-e2e.json @@ -0,0 +1,9 @@ +{ + "moduleFileExtensions": ["js", "json", "ts"], + "rootDir": ".", + "testEnvironment": "node", + "testRegex": ".e2e-spec.ts$", + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + } +} diff --git a/apps/agent-provisioning/tsconfig.app.json b/apps/agent-provisioning/tsconfig.app.json new file mode 100644 index 000000000..bd244d42c --- /dev/null +++ b/apps/agent-provisioning/tsconfig.app.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "declaration": false, + "outDir": "../../dist/apps/agent-provisioning" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] +} diff --git a/apps/agent-service/Dockerfile b/apps/agent-service/Dockerfile new file mode 100644 index 000000000..06ca34754 --- /dev/null +++ b/apps/agent-service/Dockerfile @@ -0,0 +1,43 @@ +# Stage 1: Build the application +FROM node:16-alpine as build + +# Set the working directory +WORKDIR /app + +# Copy package.json and package-lock.json +COPY package.json ./ +#COPY package-lock.json ./ + +# Install dependencies +RUN npm i + +# Copy the rest of the application code +COPY . . +CMD ["sh", "-c", "cd libs/prisma-service && npx prisma generate"] + +# Build the user service +RUN npm run build agent-service + +# Stage 2: Create the final image +FROM node:16-alpine + +# Set the working directory +WORKDIR /app + +# Copy the compiled code from the build stage +COPY --from=build /app/dist/apps/agent-service/ ./dist/apps/agent-service/ + +# 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 + +# Install production dependencies for the final image +#RUN npm i --only=production + +# Set the command to run the microservice +CMD ["sh", "-c", "cd libs/prisma-service && npx prisma generate && cd ../.. && node dist/apps/agent-service/main.js"] + +# docker build -t agent-service -f apps/agent-service/Dockerfile . +# docker run -d --env-file .env --name agent-service docker.io/library/agent-service +# docker logs -f agent-service diff --git a/apps/agent-service/src/agent-service.controller.ts b/apps/agent-service/src/agent-service.controller.ts new file mode 100644 index 000000000..caa1a54c2 --- /dev/null +++ b/apps/agent-service/src/agent-service.controller.ts @@ -0,0 +1,91 @@ +import { Controller } from '@nestjs/common'; +import { MessagePattern } from '@nestjs/microservices'; +import { AgentServiceService } from './agent-service.service'; +import { GetCredDefAgentRedirection, GetSchemaAgentRedirection, IAgentSpinupDto, IIssuanceCreateOffer, ITenantCredDef, ITenantDto, ITenantSchema } from './interface/agent-service.interface'; +import { IConnectionDetails, IUserRequestInterface } from './interface/agent-service.interface'; +import { ISendProofRequestPayload } from './interface/agent-service.interface'; + +@Controller() +export class AgentServiceController { + constructor(private readonly agentServiceService: AgentServiceService) { } + + @MessagePattern({ cmd: 'agent-spinup' }) + async walletProvision(payload: { agentSpinupDto: IAgentSpinupDto, user: IUserRequestInterface }): Promise { + return this.agentServiceService.walletProvision(payload.agentSpinupDto, payload.user); + } + + @MessagePattern({ cmd: 'create-tenant' }) + async createTenant(payload: { createTenantDto: ITenantDto, user: IUserRequestInterface }): Promise { + return this.agentServiceService.createTenant(payload.createTenantDto, payload.user); + } + + @MessagePattern({ cmd: 'agent-create-schema' }) + async createSchema(payload: ITenantSchema): Promise { + return this.agentServiceService.createSchema(payload); + } + + @MessagePattern({ cmd: 'agent-get-schema' }) + async getSchemaById(payload: GetSchemaAgentRedirection): Promise { + return this.agentServiceService.getSchemaById(payload); + } + + @MessagePattern({ cmd: 'agent-create-credential-definition' }) + async createCredentialDefinition(payload: ITenantCredDef): Promise { + return this.agentServiceService.createCredentialDefinition(payload); + } + + @MessagePattern({ cmd: 'agent-get-credential-definition' }) + async getCredentialDefinitionById(payload: GetCredDefAgentRedirection): Promise { + return this.agentServiceService.getCredentialDefinitionById(payload); + } + + + @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); + } + + @MessagePattern({ cmd: 'agent-send-credential-create-offer' }) + async sendCredentialCreateOffer(payload: { issueData: IIssuanceCreateOffer, url: string, apiKey: string }): Promise { + return this.agentServiceService.sendCredentialCreateOffer(payload.issueData, payload.url, payload.apiKey); + } + + @MessagePattern({ cmd: 'agent-get-all-issued-credentials' }) + async getIssueCredentials(payload: { url: string, apiKey: string }): Promise { + return this.agentServiceService.getIssueCredentials(payload.url, payload.apiKey); + } + + @MessagePattern({ cmd: 'agent-get-issued-credentials-by-credentialDefinitionId' }) + async getIssueCredentialsbyCredentialRecordId(payload: { url: string, apiKey: string }): Promise { + return this.agentServiceService.getIssueCredentialsbyCredentialRecordId(payload.url, payload.apiKey); + } + @MessagePattern({ cmd: 'agent-get-proof-presentations' }) + async getProofPresentations(payload: { url: string, apiKey: string }): Promise { + return this.agentServiceService.getProofPresentations(payload.url, payload.apiKey); + } + + @MessagePattern({ cmd: 'agent-get-proof-presentation-by-id' }) + async getProofPresentationById(payload: { url: string, apiKey: string }): Promise { + return this.agentServiceService.getProofPresentationById(payload.url, payload.apiKey); + } + + @MessagePattern({ cmd: 'agent-send-proof-request' }) + async sendProofRequest(payload: { proofRequestPayload: ISendProofRequestPayload, url: string, apiKey: string }): Promise { + return this.agentServiceService.sendProofRequest(payload.proofRequestPayload, payload.url, payload.apiKey); + } + + @MessagePattern({ cmd: 'agent-verify-presentation' }) + async verifyPresentation(payload: { url: string, apiKey: string }): Promise { + return this.agentServiceService.verifyPresentation(payload.url, payload.apiKey); + } + + @MessagePattern({ cmd: 'agent-get-all-connections' }) + async getConnections(payload: { url: string, apiKey: string }): Promise { + return this.agentServiceService.getConnections(payload.url, payload.apiKey); + } + + @MessagePattern({ cmd: 'agent-get-connections-by-connectionId' }) + async getConnectionsByconnectionId(payload: { url: string, apiKey: string }): Promise { + return this.agentServiceService.getConnectionsByconnectionId(payload.url, payload.apiKey); + } +} diff --git a/apps/agent-service/src/agent-service.module.ts b/apps/agent-service/src/agent-service.module.ts new file mode 100644 index 000000000..b51014f93 --- /dev/null +++ b/apps/agent-service/src/agent-service.module.ts @@ -0,0 +1,29 @@ +import { CommonModule } from '@credebl/common'; +import { PrismaService } from '@credebl/prisma-service'; +import { Logger, Module } from '@nestjs/common'; +import { ClientsModule, Transport } from '@nestjs/microservices'; +import { AgentServiceController } from './agent-service.controller'; +import { AgentServiceService } from './agent-service.service'; +import { AgentServiceRepository } from './repositories/agent-service.repository'; +import { ConfigModule } from '@nestjs/config'; +import { ConnectionService } from 'apps/connection/src/connection.service'; +import { ConnectionRepository } from 'apps/connection/src/connection.repository'; + +@Module({ + imports: [ + ConfigModule.forRoot(), + ClientsModule.register([ + { + name: 'NATS_CLIENT', + transport: Transport.NATS, + options: { + servers: [`${process.env.NATS_URL}`] + } + } + ]), + CommonModule + ], + controllers: [AgentServiceController], + providers: [AgentServiceService, AgentServiceRepository, PrismaService, Logger, ConnectionService, ConnectionRepository] +}) +export class AgentServiceModule { } diff --git a/apps/agent-service/src/agent-service.service.ts b/apps/agent-service/src/agent-service.service.ts new file mode 100644 index 000000000..1b94d1e06 --- /dev/null +++ b/apps/agent-service/src/agent-service.service.ts @@ -0,0 +1,745 @@ +/* eslint-disable no-useless-catch */ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable camelcase */ +import { + BadRequestException, + ConflictException, + HttpException, + Inject, + Injectable, + InternalServerErrorException, + Logger, + NotFoundException +} from '@nestjs/common'; +import { ClientProxy, RpcException } from '@nestjs/microservices'; +import * as dotenv from 'dotenv'; +import * as fs from 'fs'; +import { catchError, map } from 'rxjs/operators'; +dotenv.config(); +import { GetCredDefAgentRedirection, IAgentSpinupDto, IStoreOrgAgentDetails, ITenantCredDef, ITenantDto, ITenantSchema, IWalletProvision, ISendProofRequestPayload, IIssuanceCreateOffer } from './interface/agent-service.interface'; +import { AgentType, OrgAgentType } from '@credebl/enum/enum'; +import { IConnectionDetails, IUserRequestInterface } from './interface/agent-service.interface'; +import { AgentServiceRepository } from './repositories/agent-service.repository'; +import { ledgers, org_agents, organisation, platform_config } from '@prisma/client'; +import { CommonConstants } from '@credebl/common/common.constant'; +import { CommonService } from '@credebl/common'; +import { v4 as uuidv4 } from 'uuid'; +import { GetSchemaAgentRedirection } from 'apps/ledger/src/schema/schema.interface'; +import { ConnectionService } from 'apps/connection/src/connection.service'; +import { ResponseMessages } from '@credebl/common/response-messages'; +import { io } from 'socket.io-client'; +import { WebSocketGateway } from '@nestjs/websockets'; + +@Injectable() +@WebSocketGateway() +export class AgentServiceService { + + private readonly logger = new Logger('WalletService'); + + constructor( + private readonly agentServiceRepository: AgentServiceRepository, + private readonly commonService: CommonService, + private readonly connectionService: ConnectionService, + @Inject('NATS_CLIENT') private readonly agentServiceProxy: ClientProxy + + ) { } + + async ReplaceAt(input, search, replace, start, end): Promise { + return input.slice(0, start) + + input.slice(start, end).replace(search, replace) + + input.slice(end); + } + + async _validateInternalIp( + platformConfig: platform_config, + controllerIp: string + ): Promise { + let internalIp = ''; + const maxIpLength = '255'; + const indexValue = 1; + const controllerIpLength = 0; + try { + if ( + platformConfig.lastInternalId.split('.')[3] < maxIpLength && + platformConfig.lastInternalId.split('.')[3] !== maxIpLength + ) { + internalIp = await this.ReplaceAt( + controllerIp, + controllerIp.split('.')[3], + parseInt(controllerIp.split('.')[3]) + indexValue, + controllerIp.lastIndexOf('.') + indexValue, + controllerIp.length + ); + + platformConfig.lastInternalId = internalIp; + } else if ( + platformConfig.lastInternalId.split('.')[2] < maxIpLength && + platformConfig.lastInternalId.split('.')[2] !== maxIpLength + ) { + internalIp = await this.ReplaceAt( + controllerIp, + controllerIp.split('.')[2], + parseInt(controllerIp.split('.')[2]) + indexValue, + controllerIp.indexOf('.', controllerIp.indexOf('.') + indexValue) + + indexValue, + controllerIp.length + ); + + platformConfig.lastInternalId = internalIp; + } else if ( + platformConfig.lastInternalId.split('.')[1] < maxIpLength && + platformConfig.lastInternalId.split('.')[1] !== maxIpLength + ) { + internalIp = await this.ReplaceAt( + controllerIp, + controllerIp.split('.')[1], + parseInt(controllerIp.split('.')[1]) + indexValue, + controllerIp.indexOf('.', controllerIp.indexOf('.')) + indexValue, + controllerIp.length + ); + + platformConfig.lastInternalId = internalIp; + } else if ( + platformConfig.lastInternalId.split('.')[0] < maxIpLength && + platformConfig.lastInternalId.split('.')[0] !== maxIpLength + ) { + internalIp = await this.ReplaceAt( + controllerIp, + controllerIp.split('.')[0], + parseInt(controllerIp.split('.')[0]) + indexValue, + controllerIpLength, + controllerIp.length + ); + + platformConfig.lastInternalId = internalIp; + } else { + this.logger.error(`This IP address is not valid!`); + throw new BadRequestException(`This IP address is not valid!`); + } + + return internalIp; + } catch (error) { + this.logger.error(`error in valid internal ip : ${JSON.stringify(error)}`); + throw new RpcException(error.response); + } + } + + async walletProvision(agentSpinupDto: IAgentSpinupDto, user: IUserRequestInterface): Promise<{ + agentSpinupStatus: number; + }> { + try { + + agentSpinupDto.agentType = agentSpinupDto.agentType ? agentSpinupDto.agentType : 1; + agentSpinupDto.tenant = agentSpinupDto.tenant ? agentSpinupDto.tenant : false; + + const platformConfig: platform_config = await this.agentServiceRepository.getPlatformConfigDetails(); + const ledgerDetails: ledgers = await this.agentServiceRepository.getGenesisUrl(agentSpinupDto.ledgerId); + const orgData: organisation = await this.agentServiceRepository.getOrgDetails(agentSpinupDto.orgId); + + if (!orgData) { + this.logger.error(ResponseMessages.agent.error.orgNotFound); + throw new NotFoundException(ResponseMessages.agent.error.orgNotFound); + } + + const getOrgAgent = await this.agentServiceRepository.getAgentDetails(agentSpinupDto.orgId); + + if (2 === getOrgAgent?.agentSpinUpStatus) { + this.logger.error(`Agent already exists.`); + throw new NotFoundException('Agent already exists'); + } + + const orgApiKey: string = platformConfig?.sgApiKey; + + const containerName: string = uuidv4(); + + if (fs.existsSync(`./apps/agent-provisioning/AFJ/endpoints/${agentSpinupDto.orgId}_${containerName}.json`)) { + fs.unlinkSync(`./apps/agent-provisioning/AFJ/endpoints/${agentSpinupDto.orgId}_${containerName}.json`); + } + + if (!platformConfig?.apiEndpoint) { + this.logger.error(ResponseMessages.agent.error.apiEndpointNotFound); + throw new BadRequestException(ResponseMessages.agent.error.apiEndpointNotFound); + } + + const externalIp: string = platformConfig?.externalIp; + const controllerIp: string = ('false' !== platformConfig?.lastInternalId) ? (platformConfig?.lastInternalId) : ''; + + const apiEndpoint: string = platformConfig?.apiEndpoint; + const { WALLET_STORAGE_HOST } = process.env; + const { WALLET_STORAGE_PORT } = process.env; + const { WALLET_STORAGE_USER } = process.env; + const { WALLET_STORAGE_PASSWORD } = process.env; + + const internalIp: string = await this._validateInternalIp( + platformConfig, + controllerIp + ); + + if (agentSpinupDto.agentType === AgentType.ACAPY) { + + // TODO: ACA-PY Agent Spin-Up + } else if (agentSpinupDto.agentType === AgentType.AFJ) { + + const walletProvisionPayload: IWalletProvision = { + orgId: `${orgData.id}`, + externalIp, + walletName: agentSpinupDto.walletName, + walletPassword: agentSpinupDto.walletPassword, + seed: agentSpinupDto.seed, + webhookEndpoint: apiEndpoint, + walletStorageHost: WALLET_STORAGE_HOST, + walletStoragePort: WALLET_STORAGE_PORT, + walletStorageUser: WALLET_STORAGE_USER, + walletStoragePassword: WALLET_STORAGE_PASSWORD, + internalIp, + containerName, + agentType: AgentType.AFJ, + orgName: orgData.name, + genesisUrl: ledgerDetails?.poolConfig, + afjVersion: process.env.AFJ_VERSION, + protocol: process.env.API_GATEWAY_PROTOCOL, + tenant: agentSpinupDto.tenant + }; + + const socket = await io(`${process.env.SOCKET_HOST}`, { + reconnection: true, + reconnectionDelay: 5000, + reconnectionAttempts: Infinity, + autoConnect: true, + transports: ['websocket'] + }); + + if (agentSpinupDto.clientSocketId) { + socket.emit('agent-spinup-process-initiated', { clientId: agentSpinupDto.clientSocketId }); + } + + await this._agentSpinup(walletProvisionPayload, agentSpinupDto, orgApiKey, orgData, user, socket); + const agentStatusResponse = { + agentSpinupStatus: 1 + }; + + return agentStatusResponse; + } + } catch (error) { + if (agentSpinupDto.clientSocketId) { + const socket = await io(`${process.env.SOCKET_HOST}`, { + reconnection: true, + reconnectionDelay: 5000, + reconnectionAttempts: Infinity, + autoConnect: true, + transports: ['websocket'] + }); + socket.emit('error-in-wallet-creation-process', { clientId: agentSpinupDto.clientSocketId, error }); + } + this.logger.error(`Error in Agent spin up : ${JSON.stringify(error)}`); + } + } + + async _agentSpinup(walletProvisionPayload: IWalletProvision, agentSpinupDto: IAgentSpinupDto, orgApiKey: string, orgData: organisation, user: IUserRequestInterface, socket): Promise { + try { + const agentSpinUpResponse = new Promise(async (resolve, _reject) => { + + const walletProvision: { + response + } = await this._walletProvision(walletProvisionPayload); + + if (!walletProvision?.response) { + throw new InternalServerErrorException('Agent not able to spin-up'); + } else { + resolve(walletProvision?.response); + } + + return agentSpinUpResponse.then(async (agentDetails) => { + if (agentDetails) { + const controllerEndpoints = JSON.parse(agentDetails); + const agentEndPoint = `${process.env.API_GATEWAY_PROTOCOL}://${controllerEndpoints.CONTROLLER_ENDPOINT}`; + + if (agentEndPoint && agentSpinupDto.clientSocketId) { + const socket = io(`${process.env.SOCKET_HOST}`, { + reconnection: true, + reconnectionDelay: 5000, + reconnectionAttempts: Infinity, + autoConnect: true, + transports: ['websocket'] + }); + socket.emit('agent-spinup-process-completed', { clientId: agentSpinupDto.clientSocketId }); + } + + const agentPayload: IStoreOrgAgentDetails = { + agentEndPoint, + seed: agentSpinupDto.seed, + apiKey: orgApiKey, + agentsTypeId: AgentType.AFJ, + orgId: orgData.id, + walletName: agentSpinupDto.walletName, + clientSocketId: agentSpinupDto.clientSocketId + }; + + if (agentEndPoint && agentSpinupDto.clientSocketId) { + socket.emit('did-publish-process-initiated', { clientId: agentSpinupDto.clientSocketId }); + } + const storeAgentDetails = await this._storeOrgAgentDetails(agentPayload); + if (agentSpinupDto.clientSocketId) { + socket.emit('did-publish-process-completed', { clientId: agentSpinupDto.clientSocketId }); + } + + if (storeAgentDetails) { + if (agentSpinupDto.clientSocketId) { + socket.emit('invitation-url-creation-started', { clientId: agentSpinupDto.clientSocketId }); + } + await this._createLegacyConnectionInvitation(orgData.id, user); + if (agentSpinupDto.clientSocketId) { + socket.emit('invitation-url-creation-success', { clientId: agentSpinupDto.clientSocketId }); + } + } + resolve(storeAgentDetails); + } else { + throw new InternalServerErrorException('Agent not able to spin-up'); + } + }) + .catch((error) => { + _reject(error); + }); + }); + } catch (error) { + if (agentSpinupDto.clientSocketId) { + const socket = await io(`${process.env.SOCKET_HOST}`, { + reconnection: true, + reconnectionDelay: 5000, + reconnectionAttempts: Infinity, + autoConnect: true, + transports: ['websocket'] + }); + socket.emit('error-in-wallet-creation-process', { clientId: agentSpinupDto.clientSocketId, error }); + } + this.logger.error(`[_agentSpinup] - Error in Agent spin up : ${JSON.stringify(error)}`); + } + } + + async _storeOrgAgentDetails(payload: IStoreOrgAgentDetails): Promise { + try { + + + const agentDidWriteUrl = `${payload.agentEndPoint}${CommonConstants.URL_AGENT_WRITE_DID}`; + const agentDid = await this.commonService + .httpPost(agentDidWriteUrl, { seed: payload.seed }, { headers: { 'x-api-key': payload.apiKey } }) + .then(async response => response); + + if (agentDid) { + + const getDidMethodUrl = `${payload.agentEndPoint}${CommonConstants.URL_AGENT_GET_DIDS}`; + const getDidMethod = await this.commonService + .httpGet(getDidMethodUrl, { headers: { 'x-api-key': payload.apiKey } }) + .then(async response => response); + + const storeOrgAgentData: IStoreOrgAgentDetails = { + did: getDidMethod[0]?.did, + verkey: getDidMethod[0]?.didDocument?.verificationMethod[0]?.publicKeyBase58, + isDidPublic: true, + agentSpinUpStatus: 2, + walletName: payload.walletName, + agentsTypeId: AgentType.AFJ, + orgId: payload.orgId, + agentEndPoint: payload.agentEndPoint, + agentId: payload.agentId, + orgAgentTypeId: OrgAgentType.DEDICATED + }; + + + const storeAgentDid = await this.agentServiceRepository.storeOrgAgentDetails(storeOrgAgentData); + return storeAgentDid; + + } else { + throw new InternalServerErrorException('DID is not registered on the ledger'); + } + + + } catch (error) { + if (payload.clientSocketId) { + const socket = await io(`${process.env.SOCKET_HOST}`, { + reconnection: true, + reconnectionDelay: 5000, + reconnectionAttempts: Infinity, + autoConnect: true, + transports: ['websocket'] + }); + socket.emit('error-in-wallet-creation-process', { clientId: payload.clientSocketId, error }); + } + this.logger.error(`[_storeOrgAgentDetails] - Error in store agent details : ${JSON.stringify(error)}`); + throw error; + } + } + + async _createLegacyConnectionInvitation(orgId: number, user: IUserRequestInterface): Promise<{ + response; + }> { + try { + const pattern = { + cmd: 'create-connection' + }; + const payload = { orgId, user }; + 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(`error in create-connection in wallet provision : ${JSON.stringify(error)}`); + } + } + + + async _walletProvision(payload: IWalletProvision): Promise<{ + response; + }> { + try { + 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); + }); + } catch (error) { + this.logger.error(`error in wallet provision : ${JSON.stringify(error)}`); + throw error; + } + } + + async createTenant(payload: ITenantDto, user: IUserRequestInterface): Promise { + try { + const { label, seed } = payload; + const createTenantOptions = { + config: { + label + }, + seed + }; + + const platformAdminSpinnedUp = await this.agentServiceRepository.platformAdminAgent(parseInt(process.env.PLATFORM_ID)); + + if (2 !== platformAdminSpinnedUp.org_agents[0].agentSpinUpStatus) { + throw new NotFoundException('Platform-admin agent is not spun-up'); + } + + const apiKey = ''; + const url = `${platformAdminSpinnedUp.org_agents[0].agentEndPoint}${CommonConstants.URL_SHAGENT_CREATE_TENANT}`; + const tenantDetails = await this.commonService + .httpPost(url, createTenantOptions, { headers: { 'x-api-key': apiKey } }) + .then(async (tenant) => { + this.logger.debug(`API Response Data: ${JSON.stringify(tenant)}`); + return tenant; + }); + const storeOrgAgentData: IStoreOrgAgentDetails = { + did: tenantDetails.did, + verkey: tenantDetails.verkey, + isDidPublic: true, + agentSpinUpStatus: 2, + agentsTypeId: AgentType.AFJ, + orgId: payload.orgId, + agentEndPoint: platformAdminSpinnedUp.org_agents[0].agentEndPoint, + orgAgentTypeId: OrgAgentType.SHARED, + tenantId: tenantDetails.tenantRecord.id, + walletName: label + }; + + const saveTenant = await this.agentServiceRepository.storeOrgAgentDetails(storeOrgAgentData); + await this._createLegacyConnectionInvitation(payload.orgId, user); + return saveTenant; + + } catch (error) { + this.logger.error(`Error in creating tenant: ${error}`); + throw new RpcException(error.response); + } + } + + + async createSchema(payload: ITenantSchema): Promise { + try { + let schemaResponse; + + if (1 === payload.agentType) { + + const url = `${payload.agentEndPoint}${CommonConstants.URL_SCHM_CREATE_SCHEMA}`; + const schemaPayload = { + attributes: payload.attributes, + version: payload.version, + name: payload.name, + issuerId: payload.issuerId + }; + schemaResponse = await this.commonService.httpPost(url, schemaPayload, { headers: { 'x-api-key': payload.apiKey } }) + .then(async (schema) => { + this.logger.debug(`API Response Data: ${JSON.stringify(schema)}`); + return schema; + }); + + } else if (2 === payload.agentType) { + + const url = `${payload.agentEndPoint}${CommonConstants.URL_SHAGENT_WITH_TENANT_AGENT}`; + const schemaPayload = { + tenantId: payload.tenantId, + method: 'registerSchema', + payload: { + attributes: payload.payload.attributes, + version: payload.payload.version, + name: payload.payload.name, + issuerId: payload.payload.issuerId + } + }; + schemaResponse = await this.commonService.httpPost(url, schemaPayload, { headers: { 'x-api-key': payload.apiKey } }) + .then(async (schema) => { + this.logger.debug(`API Response Data: ${JSON.stringify(schema)}`); + return schema; + }); + } + return schemaResponse; + } catch (error) { + this.logger.error(`Error in creating schema: ${error}`); + throw error; + } + } + + async getSchemaById(payload: GetSchemaAgentRedirection): Promise { + try { + let schemaResponse; + + if (1 === 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) => { + this.logger.debug(`API Response Data: ${JSON.stringify(schema)}`); + return schema; + }); + + } else if (2 === payload.agentType) { + const url = `${payload.agentEndPoint}${CommonConstants.URL_SHAGENT_WITH_TENANT_AGENT}`; + const schemaPayload = { + tenantId: payload.tenantId, + method: payload.method, + payload: { + 'schemaId': `${payload.payload.schemaId}` + } + }; + schemaResponse = await this.commonService.httpPost(url, schemaPayload, { headers: { 'x-api-key': payload.apiKey } }) + .then(async (schema) => { + this.logger.debug(`API Response Data: ${JSON.stringify(schema)}`); + return schema; + }); + } + return schemaResponse; + } catch (error) { + this.logger.error(`Error in getting schema: ${error}`); + throw error; + } + } + + async createCredentialDefinition(payload: ITenantCredDef): Promise { + try { + let credDefResponse; + if (1 === payload.agentType) { + const url = `${payload.agentEndPoint}${CommonConstants.URL_SCHM_CREATE_CRED_DEF}`; + const credDefPayload = { + tag: payload.tag, + schemaId: payload.schemaId, + issuerId: payload.issuerId + }; + credDefResponse = await this.commonService.httpPost(url, credDefPayload, { headers: { 'x-api-key': payload.apiKey } }) + .then(async (credDef) => { + this.logger.debug(`API Response Data: ${JSON.stringify(credDef)}`); + return credDef; + }); + + } else if (2 === payload.agentType) { + const url = `${payload.agentEndPoint}${CommonConstants.URL_SHAGENT_WITH_TENANT_AGENT}`; + const credDefPayload = { + tenantId: payload.tenantId, + method: 'registerCredentialDefinition', + payload: { + tag: payload.payload.tag, + schemaId: payload.payload.schemaId, + issuerId: payload.payload.issuerId + } + }; + credDefResponse = await this.commonService.httpPost(url, credDefPayload, { headers: { 'x-api-key': payload.apiKey } }) + .then(async (credDef) => { + this.logger.debug(`API Response Data: ${JSON.stringify(credDef)}`); + return credDef; + }); + } + return credDefResponse; + } catch (error) { + this.logger.error(`Error in creating credential definition: ${error}`); + throw error; + } + } + + async getCredentialDefinitionById(payload: GetCredDefAgentRedirection): Promise { + try { + let credDefResponse; + + if (1 === 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) => { + this.logger.debug(`API Response Data: ${JSON.stringify(credDef)}`); + return credDef; + }); + + } else if (2 === payload.agentType) { + const url = `${payload.agentEndPoint}${CommonConstants.URL_SHAGENT_WITH_TENANT_AGENT}`; + const credDefPayload = { + tenantId: payload.tenantId, + method: payload.method, + payload: { + 'credentialDefinitionId': `${payload.payload.credentialDefinitionId}` + } + }; + credDefResponse = await this.commonService.httpPost(url, credDefPayload, { headers: { 'x-api-key': payload.apiKey } }) + .then(async (credDef) => { + this.logger.debug(`API Response Data: ${JSON.stringify(credDef)}`); + return credDef; + }); + } + return credDefResponse; + } catch (error) { + this.logger.error(`Error in getting schema: ${error}`); + throw error; + } + } + + async createLegacyConnectionInvitation(connectionPayload: IConnectionDetails, url: string, apiKey: string): Promise { + try { + const data = await this.commonService + .httpPost(url, connectionPayload, { headers: { 'x-api-key': apiKey } }) + .then(async response => response); + return data; + } catch (error) { + this.logger.error(`Error in connection Invitation in agent service : ${JSON.stringify(error)}`); + throw new RpcException(error); + } + } + + async sendCredentialCreateOffer(issueData: IIssuanceCreateOffer, url: string, apiKey: string): Promise { + try { + const data = await this.commonService + .httpPost(url, issueData, { headers: { 'x-api-key': apiKey } }) + .then(async response => response); + return data; + } catch (error) { + this.logger.error(`Error in sendCredentialCreateOffer in agent service : ${JSON.stringify(error)}`); + } + } + async getProofPresentations(url: string, apiKey: string): Promise { + try { + const getProofPresentationsData = await this.commonService + .httpGet(url, { headers: { 'x-api-key': apiKey } }) + .then(async response => response); + return getProofPresentationsData; + } catch (error) { + this.logger.error(`Error in proof presentations in agent service : ${JSON.stringify(error)}`); + throw new RpcException(error); + } + } + + async getIssueCredentials(url: string, apiKey: string): Promise { + try { + const data = await this.commonService + .httpGet(url, { headers: { 'x-api-key': apiKey } }) + .then(async response => response); + return data; + } catch (error) { + this.logger.error(`Error in getIssueCredentials in agent service : ${JSON.stringify(error)}`); + } + } + async getProofPresentationById(url: string, apiKey: string): Promise { + try { + const getProofPresentationById = await this.commonService + .httpGet(url, { headers: { 'x-api-key': apiKey } }) + .then(async response => response); + return getProofPresentationById; + } catch (error) { + this.logger.error(`Error in proof presentation by id in agent service : ${JSON.stringify(error)}`); + throw new RpcException(error); + } + } + + async getIssueCredentialsbyCredentialRecordId(url: string, apiKey: string): Promise { + try { + const data = await this.commonService + .httpGet(url, { headers: { 'x-api-key': apiKey } }) + .then(async response => response); + return data; + } catch (error) { + this.logger.error(`Error in getIssueCredentialsbyCredentialRecordId in agent service : ${JSON.stringify(error)}`); + } + } + async sendProofRequest(proofRequestPayload: ISendProofRequestPayload, url: string, apiKey: string): Promise { + try { + const sendProofRequest = await this.commonService + .httpPost(url, proofRequestPayload, { headers: { 'x-api-key': apiKey } }) + .then(async response => response); + return sendProofRequest; + } catch (error) { + this.logger.error(`Error in send proof request in agent service : ${JSON.stringify(error)}`); + throw new RpcException(error); + } + } + + async verifyPresentation(url: string, apiKey: string): Promise { + try { + const verifyPresentation = await this.commonService + .httpPost(url, '', { headers: { 'x-api-key': apiKey } }) + .then(async response => response); + return verifyPresentation; + } catch (error) { + this.logger.error(`Error in verify proof presentation in agent service : ${JSON.stringify(error)}`); + throw new RpcException(error); + } + } + + async getConnections(url: string, apiKey: string): Promise { + try { + const data = await this.commonService + .httpGet(url, { headers: { 'x-api-key': apiKey } }) + .then(async response => response); + return data; + } catch (error) { + this.logger.error(`Error in getConnections in agent service : ${JSON.stringify(error)}`); + } + } + + async getConnectionsByconnectionId(url: string, apiKey: string): Promise { + try { + const data = await this.commonService + .httpGet(url, { headers: { 'x-api-key': apiKey } }) + .then(async response => response); + return data; + } catch (error) { + this.logger.error(`Error in getConnectionsByconnectionId in agent service : ${JSON.stringify(error)}`); + } + } +} + diff --git a/apps/agent-service/src/interface/agent-service.interface.ts b/apps/agent-service/src/interface/agent-service.interface.ts new file mode 100644 index 000000000..3e00e2245 --- /dev/null +++ b/apps/agent-service/src/interface/agent-service.interface.ts @@ -0,0 +1,275 @@ +import { AgentType, OrgAgentType } from '@credebl/enum/enum'; +import { UserRoleOrgPermsDto } from 'apps/api-gateway/src/dtos/user-role-org-perms.dto'; + +export interface IAgentSpinupDto { + + walletName: string; + walletPassword: string; + seed: string; + orgId: number; + agentType?: AgentType; + ledgerId?: number; + transactionApproval?: boolean; + clientSocketId?: string + tenant?: boolean; +} + +export interface ITenantDto { + label: string; + seed: string; + tenantId?: string; + orgId: number; +} + +export interface ITenantSchema { + tenantId?: string; + attributes: string[]; + version: string; + name: string; + issuerId?: string; + payload?: ITenantSchemaDto; + method?: string; + agentType?: number; + apiKey?: string; + agentEndPoint?: string; +} + +export interface ITenantSchemaDto { + attributes: string[]; + version: string; + name: string; + issuerId: string; +} + +export interface GetSchemaAgentRedirection { + schemaId?: string; + tenantId?: string; + payload?: GetSchemaFromTenantPayload; + apiKey?: string; + agentEndPoint?: string; + agentType?: number; + method?: string; +} + +export interface GetSchemaFromTenantPayload { + schemaId: string; +} + +export interface ITenantCredDef { + tenantId?: string; + tag?: string; + schemaId?: string; + issuerId?: string; + payload?: ITenantCredDef; + method?: string; + agentType?: number; + apiKey?: string; + agentEndPoint?: string; +} + +export interface ITenantCredDefDto { + tag: string; + schemaId: string; + issuerId: string; +} + +export interface GetCredDefAgentRedirection { + credentialDefinitionId?: string; + tenantId?: string; + payload?: GetCredDefFromTenantPayload; + apiKey?: string; + agentEndPoint?: string; + agentType?: number; + method?: string; +} + +export interface GetCredDefFromTenantPayload { + credentialDefinitionId: string; +} + +export interface IWalletProvision { + orgId: string; + externalIp: string; + walletName: string; + walletPassword: string; + seed: string; + webhookEndpoint: string; + walletStorageHost: string; + walletStoragePort: string; + walletStorageUser: string; + walletStoragePassword: string; + internalIp: string; + containerName: string; + agentType: AgentType; + orgName: string; + genesisUrl: string; + afjVersion: string; + protocol: string; + tenant: boolean; +} + +export interface IPlatformConfigDto { + externalIP: string; + genesisURL: string; + adminKey: string; + lastInternalIP: string; + platformTestNetApiKey: string; + sgEmailFrom: string; + apiEndpoint: string; + tailsFileServer: string; +} + +export interface IStoreOrgAgentDetails { + clientSocketId?: string; + agentEndPoint?: string; + apiKey?: string; + seed?: string; + did?: string; + verkey?: string; + isDidPublic?: boolean; + agentSpinUpStatus?: number; + walletName?: string; + agentsTypeId?: AgentType; + orgId?: number; + agentId?: number; + orgAgentTypeId?: OrgAgentType; + tenantId?: string; +} + + +export interface IConnectionDetails { + multiUseInvitation?: boolean; + autoAcceptConnection: boolean; +} + +export interface IUserRequestInterface { + userId: number; + email: string; + orgId: number; + agentEndPoint?: string; + apiKey?: string; + tenantId?: number; + tenantName?: string; + tenantOrgId?: number; + userRoleOrgPermissions?: UserRoleOrgPermsDto[]; + orgName?: string; + selectedOrg: ISelectedOrgInterface; +} + +export interface ISelectedOrgInterface { + id: number; + userId: number; + orgRoleId: number; + orgId: number; + orgRole: object; + organisation: IOrganizationInterface; +} + +export interface IOrganizationInterface { + name: string; + description: string; + org_agents: IOrgAgentInterface[] + +} + +export interface IOrgAgentInterface { + orgDid: string; + verkey: string; + agentEndPoint: string; + agentOptions: string; + walletName: string; + agentsTypeId: string; + orgId: string; +} + +export interface ITenantCredDef { + tenantId?: string; + tag?: string; + schemaId?: string; + issuerId?: string; + payload?: ITenantCredDef; + method?: string; + agentType?: number; + apiKey?: string; + agentEndPoint?: string; +} + +export interface ITenantCredDefDto { + tag: string; + schemaId: string; + issuerId: string; +} + +export interface GetCredDefAgentRedirection { + credentialDefinitionId?: string; + tenantId?: string; + payload?: GetCredDefFromTenantPayload; + apiKey?: string; + agentEndPoint?: string; + agentType?: number; + method?: string; +} + +export interface GetCredDefFromTenantPayload { + credentialDefinitionId: string; +} + +export interface IIssuanceCreateOffer { + connectionId: string; + credentialFormats: ICredentialFormats; + autoAcceptCredential: string; + comment: string; +} + +export interface ICredentialFormats { + indy: IIndy; + credentialDefinitionId: string; +} + +export interface IIndy { + attributes: IAttributes[]; +} + +export interface IAttributes { + name: string; + value: string; +} +export interface ISendProofRequestPayload { + comment: string; + connectionId: string; + proofFormats: IProofFormats; + autoAcceptProof: string; +} + +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; + restrictions: IRequestedRestriction[] +} + +interface IRequestedPredicates { + [key: string]: IRequestedPredicatesName; +} + +interface IRequestedPredicatesName { + name: string; + restrictions: IRequestedRestriction[] +} + +interface IRequestedRestriction { + cred_def_id: string; +} \ No newline at end of file diff --git a/apps/agent-service/src/main.ts b/apps/agent-service/src/main.ts new file mode 100644 index 000000000..d936711a9 --- /dev/null +++ b/apps/agent-service/src/main.ts @@ -0,0 +1,39 @@ +import { HttpExceptionFilter } from 'libs/http-exception.filter'; +import { Logger } from '@nestjs/common'; +import { NestFactory } from '@nestjs/core'; +import { AgentServiceModule } from './agent-service.module'; +import { AgentServiceService } from './agent-service.service'; +import { IAgentSpinupDto, IUserRequestInterface } from './interface/agent-service.interface'; +import { MicroserviceOptions, Transport } from '@nestjs/microservices'; + +const logger = new Logger(); + +async function bootstrap(): Promise { + + const app = await NestFactory.createMicroservice(AgentServiceModule, { + transport: Transport.NATS, + options: { + servers: [`${process.env.NATS_URL}`] + } + }); + app.useGlobalFilters(new HttpExceptionFilter()); + + await app.listen(); + logger.log('Agent-Service Microservice is listening to NATS '); + + let user: IUserRequestInterface; + const agentSpinupPayload: IAgentSpinupDto = { + walletName: process.env.PLATFORM_WALLET_NAME, + walletPassword: process.env.PLATFORM_WALLET_PASSWORD, + seed: process.env.PLATFORM_SEED, + orgId: parseInt(process.env.PLATFORM_ID), + tenant: true + }; + + const agentService = app.get(AgentServiceService); + await agentService.walletProvision(agentSpinupPayload, user) + .catch((error) => { + logger.error(error?.error?.response?.message); + }); +} +bootstrap(); \ No newline at end of file diff --git a/apps/agent-service/src/repositories/agent-service.repository.ts b/apps/agent-service/src/repositories/agent-service.repository.ts new file mode 100644 index 000000000..960a31e78 --- /dev/null +++ b/apps/agent-service/src/repositories/agent-service.repository.ts @@ -0,0 +1,137 @@ +import { PrismaService } from '@credebl/prisma-service'; +import { Injectable, InternalServerErrorException, Logger } from '@nestjs/common'; +// eslint-disable-next-line camelcase +import { ledgers, org_agents, organisation, platform_config } from '@prisma/client'; +import { IStoreOrgAgentDetails } from '../interface/agent-service.interface'; + +@Injectable() +export class AgentServiceRepository { + constructor(private readonly prisma: PrismaService, private readonly logger: Logger) { } + + /** + * Get platform config details + * @returns + */ + // eslint-disable-next-line camelcase + async getPlatformConfigDetails(): Promise { + try { + + return this.prisma.platform_config.findFirst(); + + } catch (error) { + this.logger.error(`[getPlatformConfigDetails] - error: ${JSON.stringify(error)}`); + throw new InternalServerErrorException(error); + } + } + + /** + * Get genesis url + * @param id + * @returns + */ + async getGenesisUrl(id: number): Promise { + try { + + const genesisData = await this.prisma.ledgers.findFirst({ + where: { + id + } + }); + return genesisData; + } catch (error) { + this.logger.error(`[getGenesisUrl] - get genesis URL: ${JSON.stringify(error)}`); + throw new InternalServerErrorException(error); + } + } + + /** + * Get organization details + * @param id + * @returns + */ + async getOrgDetails(id: number): Promise { + try { + + const oranizationDetails = await this.prisma.organisation.findFirst({ + where: { + id + } + }); + return oranizationDetails; + } catch (error) { + this.logger.error(`[getOrgDetails] - get organization details: ${JSON.stringify(error)}`); + throw new InternalServerErrorException(error); + } + } + + + /** + * Store agent details + * @param storeAgentDetails + * @returns + */ + // eslint-disable-next-line camelcase + async storeOrgAgentDetails(storeOrgAgentDetails: IStoreOrgAgentDetails): Promise { + try { + return this.prisma.org_agents.create({ + data: { + orgDid: storeOrgAgentDetails.did, + verkey: storeOrgAgentDetails.verkey, + isDidPublic: storeOrgAgentDetails.isDidPublic, + agentSpinUpStatus: storeOrgAgentDetails.agentSpinUpStatus, + walletName: storeOrgAgentDetails.walletName, + agentsTypeId: storeOrgAgentDetails.agentsTypeId, + orgId: storeOrgAgentDetails.orgId, + agentEndPoint: storeOrgAgentDetails.agentEndPoint, + agentId: storeOrgAgentDetails.agentId ? storeOrgAgentDetails.agentId : null, + orgAgentTypeId: storeOrgAgentDetails.orgAgentTypeId ? storeOrgAgentDetails.orgAgentTypeId : null, + tenantId: storeOrgAgentDetails.tenantId ? storeOrgAgentDetails.tenantId : null + + } + }); + } catch (error) { + this.logger.error(`[storeAgentDetails] - store agent details: ${JSON.stringify(error)}`); + throw new InternalServerErrorException(error); + } + } + + /** + * Get agent details + * @param orgId + * @returns + */ + // eslint-disable-next-line camelcase + async getAgentDetails(orgId: number): Promise { + try { + + return this.prisma.org_agents.findFirst({ + where: { + orgId + } + }); + } catch (error) { + this.logger.error(`[getAgentDetails] - get agent details: ${JSON.stringify(error)}`); + throw new InternalServerErrorException(error); + } + } + + async platformAdminAgent(platformId: number): Promise { + try { + const platformAdminSpinnedUp = await this.prisma.organisation.findUnique({ + where: { + id: platformId + }, + include: { + // eslint-disable-next-line camelcase + org_agents: true + } + }); + return platformAdminSpinnedUp; + } catch (error) { + + } + } +} \ No newline at end of file diff --git a/apps/agent-service/test/app.e2e-spec.ts b/apps/agent-service/test/app.e2e-spec.ts new file mode 100644 index 000000000..58f95a822 --- /dev/null +++ b/apps/agent-service/test/app.e2e-spec.ts @@ -0,0 +1,24 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { INestApplication } from '@nestjs/common'; +import * as request from 'supertest'; +import { AgentServiceModule } from './../src/agent-service.module'; + +describe('AgentServiceController (e2e)', () => { + let app: INestApplication; + + beforeEach(async () => { + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [AgentServiceModule] + }).compile(); + + app = moduleFixture.createNestApplication(); + await app.init(); + }); + + it('/ (GET)', () => { + return request(app.getHttpServer()) + .get('/') + .expect(200) + .expect('Hello World!'); + }); +}); diff --git a/apps/agent-service/test/jest-e2e.json b/apps/agent-service/test/jest-e2e.json new file mode 100644 index 000000000..e9d912f3e --- /dev/null +++ b/apps/agent-service/test/jest-e2e.json @@ -0,0 +1,9 @@ +{ + "moduleFileExtensions": ["js", "json", "ts"], + "rootDir": ".", + "testEnvironment": "node", + "testRegex": ".e2e-spec.ts$", + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + } +} diff --git a/apps/agent-service/tsconfig.app.json b/apps/agent-service/tsconfig.app.json new file mode 100644 index 000000000..93e89a6b9 --- /dev/null +++ b/apps/agent-service/tsconfig.app.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "declaration": false, + "outDir": "../../dist/apps/agent-service" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] +} diff --git a/apps/api-gateway/Dockerfile b/apps/api-gateway/Dockerfile new file mode 100644 index 000000000..5d721a1ec --- /dev/null +++ b/apps/api-gateway/Dockerfile @@ -0,0 +1,42 @@ +# Stage 1: Build the application +FROM node:16-alpine as build + +# Set the working directory +WORKDIR /app + +# Copy package.json and package-lock.json +COPY package.json ./ +#COPY package-lock.json ./ + +# Install dependencies +RUN npm i + +# Copy the rest of the application code +COPY . . +CMD ["sh", "-c", "cd libs/prisma-service && npx prisma generate"] + +# Build the user service +RUN npm run build api-gateway + +# Stage 2: Create the final image +FROM node:16-alpine + +# Set the working directory +WORKDIR /app + +# Copy the compiled code from the build stage +COPY --from=build /app/dist/apps/api-gateway/ ./dist/apps/api-gateway/ + +# 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 + +# Install production dependencies for the final image +#RUN npm i --only=production + +# Set the command to run the microservice +CMD ["sh", "-c", "cd libs/prisma-service && npx prisma generate && cd ../.. && node dist/apps/api-gateway/main.js"] + +# docker build -t api-gateway -f apps/api-gateway/Dockerfile . +# docker run -d --env-file .env --name api-gateway docker.io/library/api-gateway diff --git a/apps/api-gateway/common/exception-handler.ts b/apps/api-gateway/common/exception-handler.ts new file mode 100644 index 000000000..8ce4b3c75 --- /dev/null +++ b/apps/api-gateway/common/exception-handler.ts @@ -0,0 +1,49 @@ +import { + ExceptionFilter, + Catch, + ArgumentsHost, + HttpException, + HttpStatus, + HttpAdapterHost +} from '@nestjs/common'; +import { ResponseType } from './interface'; + +interface CustomException { + message: string; + error?: string; + // Add other properties if needed +} +@Catch() +export class ExceptionHandler implements ExceptionFilter { + constructor(private readonly httpAdapterHost: HttpAdapterHost) { } + + catch(exception: CustomException, host: ArgumentsHost): void { + // In certain situations `httpAdapter` might not be available in the + // constructor method, thus we should resolve it here. + const { httpAdapter } = this.httpAdapterHost; + + const ctx = host.switchToHttp(); + const response = ctx.getResponse(); + + let statusCode = HttpStatus.INTERNAL_SERVER_ERROR; + let message = + exception.message['error'] || + exception.message || + 'Something went wrong!'; + + if (exception instanceof HttpException) { + const errorResponse = exception.getResponse(); + + statusCode = exception.getStatus(); + message = errorResponse['error'] || message; + } + + const responseBody: ResponseType = { + statusCode, + message, + error: exception.message + }; + + httpAdapter.reply(response, responseBody, statusCode); + } +} diff --git a/apps/api-gateway/common/interface.ts b/apps/api-gateway/common/interface.ts new file mode 100644 index 000000000..a64429f45 --- /dev/null +++ b/apps/api-gateway/common/interface.ts @@ -0,0 +1,6 @@ +export interface ResponseType { + statusCode: number; + message: string; + data?: Record | string; + error?: Record | string; +} diff --git a/apps/api-gateway/src/agent-service/agent-service.controller.spec.ts b/apps/api-gateway/src/agent-service/agent-service.controller.spec.ts new file mode 100644 index 000000000..c873b7321 --- /dev/null +++ b/apps/api-gateway/src/agent-service/agent-service.controller.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AgentController } from './agent.controller'; + +describe('Agent Controller', () => { + let controller: AgentController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [AgentController] + }).compile(); + + controller = module.get(AgentController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/apps/api-gateway/src/agent-service/agent-service.controller.ts b/apps/api-gateway/src/agent-service/agent-service.controller.ts new file mode 100644 index 000000000..053891836 --- /dev/null +++ b/apps/api-gateway/src/agent-service/agent-service.controller.ts @@ -0,0 +1,96 @@ +/* eslint-disable no-param-reassign */ +/* eslint-disable camelcase */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { + Controller, + Logger, + Post, + UseGuards, + BadRequestException, + Body, + HttpStatus, + Res +} from '@nestjs/common'; +import { ApiTags, ApiResponse, ApiOperation, ApiUnauthorizedResponse, ApiForbiddenResponse, ApiBearerAuth } from '@nestjs/swagger'; +import { GetUser } from '../authz/decorators/get-user.decorator'; +import { AuthGuard } from '@nestjs/passport'; +import { UnauthorizedErrorDto } from '../dtos/unauthorized-error.dto'; +import { ApiResponseDto } from '../dtos/apiResponse.dto'; +import { ForbiddenErrorDto } from '../dtos/forbidden-error.dto'; +import { ResponseMessages } from '@credebl/common/response-messages'; +import { AgentService } from './agent-service.service'; +import IResponseType from '@credebl/common/interfaces/response.interface'; +import { AgentSpinupDto } from './dto/agent-service.dto'; +import { Response } from 'express'; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import { user } from '@prisma/client'; +import { CreateTenantDto } from './dto/create-tenant.dto'; +@Controller('agent-service') +@ApiTags('agents') +@UseGuards(AuthGuard('jwt')) +@ApiBearerAuth() +@ApiUnauthorizedResponse({ status: 401, description: 'Unauthorized', type: UnauthorizedErrorDto }) +@ApiForbiddenResponse({ status: 403, description: 'Forbidden', type: ForbiddenErrorDto }) +export class AgentController { + constructor(private readonly agentService: AgentService) { } + + private readonly logger = new Logger(); + + /** + * + * @param agentSpinupDto + * @param user + * @returns + */ + @Post('/spinup') + @ApiOperation({ + summary: 'Agent spinup', + description: 'Create a new agent spin up.' + }) + @ApiResponse({ status: 201, description: 'Success', type: ApiResponseDto }) + async agentSpinup( + @Body() agentSpinupDto: AgentSpinupDto, + @GetUser() user: user, + @Res() res: Response + ): Promise>> { + + const regex = new RegExp('^[a-zA-Z0-9]+$'); + if (!regex.test(agentSpinupDto.walletName)) { + this.logger.error(`Wallet name in wrong format.`); + throw new BadRequestException(`Please enter valid wallet name, It allows only alphanumeric values`); + } + this.logger.log(`**** Spin up the agent...${JSON.stringify(agentSpinupDto)}`); + const agentDetails = await this.agentService.agentSpinup(agentSpinupDto, user); + + const finalResponse: IResponseType = { + statusCode: HttpStatus.CREATED, + message: ResponseMessages.agent.success.create, + data: agentDetails.response + }; + + return res.status(HttpStatus.CREATED).json(finalResponse); + } + + @Post('/tenant') + @ApiOperation({ + summary: 'Shared Agent', + description: 'Create a shared agent.' + }) + @ApiResponse({ status: 201, description: 'Success', type: ApiResponseDto }) + async createTenant( + @Body() createTenantDto: CreateTenantDto, + @GetUser() user: user, + @Res() res: Response + ): Promise { + const tenantDetails = await this.agentService.createTenant(createTenantDto, user); + + const finalResponse: IResponseType = { + statusCode: HttpStatus.CREATED, + message: ResponseMessages.agent.success.create, + data: tenantDetails.response + }; + + return res.status(HttpStatus.CREATED).json(finalResponse); + } + +} diff --git a/apps/api-gateway/src/agent-service/agent-service.module.ts b/apps/api-gateway/src/agent-service/agent-service.module.ts new file mode 100644 index 000000000..4662016a0 --- /dev/null +++ b/apps/api-gateway/src/agent-service/agent-service.module.ts @@ -0,0 +1,28 @@ +import { Module } from '@nestjs/common'; +import { HttpModule } from '@nestjs/axios'; +import { ClientsModule, Transport } from '@nestjs/microservices'; +import { CommonModule } from '../../../../libs/common/src/common.module'; +import { CommonService } from '../../../../libs/common/src/common.service'; +import { ConfigModule } from '@nestjs/config'; +import { AgentController } from './agent-service.controller'; +import { AgentService } from './agent-service.service'; + +@Module({ + imports: [ + HttpModule, + ConfigModule.forRoot(), + ClientsModule.register([ + { + name: 'NATS_CLIENT', + transport: Transport.NATS, + options: { + servers: [`${process.env.NATS_URL}`] + } + }, + CommonModule + ]) + ], + controllers: [AgentController], + providers: [AgentService, CommonService] +}) +export class AgentModule { } diff --git a/apps/api-gateway/src/agent-service/agent-service.service.ts b/apps/api-gateway/src/agent-service/agent-service.service.ts new file mode 100644 index 000000000..5f86ed65b --- /dev/null +++ b/apps/api-gateway/src/agent-service/agent-service.service.ts @@ -0,0 +1,26 @@ +import { Injectable, Inject } from '@nestjs/common'; +import { ClientProxy } from '@nestjs/microservices'; +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'; + +@Injectable() +export class AgentService extends BaseService { + constructor( + @Inject('NATS_CLIENT') private readonly agentServiceProxy: ClientProxy + ) { + super('AgentService'); + } + + async agentSpinup(agentSpinupDto: AgentSpinupDto, user: user): Promise<{ response: object }> { + const payload = { agentSpinupDto, user }; + return this.sendNats(this.agentServiceProxy, 'agent-spinup', payload); + } + + async createTenant(createTenantDto: CreateTenantDto, user: user): Promise<{ response: object }> { + const payload = { createTenantDto, user }; + return this.sendNats(this.agentServiceProxy, 'create-tenant', payload); + } + +} diff --git a/apps/api-gateway/src/agent-service/agent.service.spec.ts b/apps/api-gateway/src/agent-service/agent.service.spec.ts new file mode 100644 index 000000000..2b7709ccc --- /dev/null +++ b/apps/api-gateway/src/agent-service/agent.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AgentService } from './agent.service'; + +describe('AgentService', () => { + let service: AgentService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [AgentService] + }).compile(); + + service = module.get(AgentService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); 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 new file mode 100644 index 000000000..c05383e9e --- /dev/null +++ b/apps/api-gateway/src/agent-service/dto/agent-service.dto.ts @@ -0,0 +1,71 @@ +import { trim } from '@credebl/common/cast.helper'; +import { ApiProperty } from '@nestjs/swagger'; +import { Transform } from 'class-transformer'; +import { IsBoolean, IsNotEmpty, IsNumber, IsOptional, IsString, Matches, MaxLength, MinLength } from 'class-validator'; +const regex = /^[a-zA-Z0-9 ]*$/; +export class AgentSpinupDto { + + @ApiProperty() + @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.' }) + @Matches(regex, { message: 'Wallet name must not contain special characters.' }) + @Matches(/^\S*$/, { + message: 'Spaces are not allowed in wallet name' + }) + walletName: string; + + @ApiProperty() + @Transform(({ value }) => trim(value)) + @IsNotEmpty({ message: 'Password is required.' }) + walletPassword: string; + + + @ApiProperty() + @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() + @IsNumber() + orgId: number; + + @ApiProperty() + @IsOptional() + @IsNumber() + ledgerId?: number; + + @ApiProperty() + @IsOptional() + clientSocketId?: string; + + @ApiProperty() + @IsOptional() + @IsBoolean() + tenant?: boolean; + + @ApiProperty() + @IsOptional() + @Transform(({ value }) => trim(value)) + @IsNotEmpty({ message: 'agentType is required'}) + @MinLength(2, { message: 'agentType must be at least 2 characters.' }) + @MaxLength(50, { message: 'agentType must be at most 50 characters.' }) + @IsString({ message: 'agentType must be in string format.' }) + agentType?: string; + + @ApiProperty() + @IsOptional() + @Transform(({ value }) => trim(value)) + @IsNotEmpty({ message: 'transactionApproval is required'}) + @MinLength(2, { message: 'transactionApproval must be at least 2 characters.' }) + @MaxLength(50, { message: 'transactionApproval must be at most 50 characters.' }) + @IsString({ message: 'transactionApproval must be in string format.' }) + transactionApproval?: string; +} diff --git a/apps/api-gateway/src/agent-service/dto/create-schema.dto.ts b/apps/api-gateway/src/agent-service/dto/create-schema.dto.ts new file mode 100644 index 000000000..430d95c51 --- /dev/null +++ b/apps/api-gateway/src/agent-service/dto/create-schema.dto.ts @@ -0,0 +1,27 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsString, IsNotEmpty, IsArray, IsNumber } from 'class-validator'; + +export class CreateTenantSchemaDto { + @ApiProperty() + @IsString({ message: 'tenantId must be a string' }) @IsNotEmpty({ message: 'please provide valid tenantId' }) + tenantId: string; + + @ApiProperty() + @IsString({ message: 'schema version must be a string' }) @IsNotEmpty({ message: 'please provide valid schema version' }) + schemaVersion: string; + + @ApiProperty() + @IsString({ message: 'schema name must be a string' }) @IsNotEmpty({ message: 'please provide valid schema name' }) + schemaName: string; + + @ApiProperty() + @IsArray({ message: 'attributes must be an array' }) + @IsString({ each: true }) + @IsNotEmpty({ message: 'please provide valid attributes' }) + attributes: string[]; + + @ApiProperty() + @IsNumber() + @IsNotEmpty({ message: 'please provide orgId' }) + orgId: number; +} \ 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 new file mode 100644 index 000000000..b51d8debd --- /dev/null +++ b/apps/api-gateway/src/agent-service/dto/create-tenant.dto.ts @@ -0,0 +1,31 @@ +import { trim } from '@credebl/common/cast.helper'; +import { ApiProperty } from '@nestjs/swagger'; +import { Transform } from 'class-transformer'; +import { IsNotEmpty, IsNumber, IsString, Matches, MaxLength, MinLength } from 'class-validator'; +const labelRegex = /^[a-zA-Z0-9 ]*$/; +export class CreateTenantDto { + @ApiProperty() + @IsString() + @Transform(({ value }) => value.trim()) + @MaxLength(25, { message: 'Maximum length for label must be 25 characters.' }) + @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() + @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() + @IsNumber() + orgId: number; +} \ No newline at end of file diff --git a/apps/api-gateway/src/agent/agent.controller.spec.ts b/apps/api-gateway/src/agent/agent.controller.spec.ts new file mode 100644 index 000000000..c873b7321 --- /dev/null +++ b/apps/api-gateway/src/agent/agent.controller.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AgentController } from './agent.controller'; + +describe('Agent Controller', () => { + let controller: AgentController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [AgentController] + }).compile(); + + controller = module.get(AgentController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/apps/api-gateway/src/agent/agent.controller.ts b/apps/api-gateway/src/agent/agent.controller.ts new file mode 100644 index 000000000..72fcb5466 --- /dev/null +++ b/apps/api-gateway/src/agent/agent.controller.ts @@ -0,0 +1,289 @@ +/* eslint-disable no-param-reassign */ +/* eslint-disable camelcase */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { + Controller, + Logger, + Get, + Post, + Query, + Param, + UseGuards, + BadRequestException, + Body, + SetMetadata +} from '@nestjs/common'; +import { AgentService } from './agent.service'; +import { ApiTags, ApiResponse, ApiOperation, ApiQuery, ApiBearerAuth, ApiParam, ApiUnauthorizedResponse, ApiForbiddenResponse, ApiExcludeEndpoint } from '@nestjs/swagger'; +import { GetUser } from '../authz/decorators/get-user.decorator'; +import { AuthGuard } from '@nestjs/passport'; +import { WalletDetailsDto } from '../dtos/wallet-details.dto'; +import { UnauthorizedErrorDto } from '../dtos/unauthorized-error.dto'; +import { AgentActions } from '../dtos/enums'; +import { RolesGuard } from '../authz/roles.guard'; +import { CommonConstants } from '@credebl/common/common.constant'; +import { booleanStatus, sortValue } from '../enum'; +import { ApiResponseDto } from '../dtos/apiResponse.dto'; +import { ForbiddenErrorDto } from '../dtos/forbidden-error.dto'; +import { CommonService } from '@credebl/common'; +import { IUserRequestInterface } from '../interfaces/IUserRequestInterface'; + +@ApiBearerAuth() +@Controller('agent') +export class AgentController { + constructor(private readonly agentService: AgentService, + private readonly commonService: CommonService) { } + + private readonly logger = new Logger(); + + /** + * + * @param user + * @param _public + * @param verkey + * @param did + * @returns List of all the DID created for the current Cloud Agent. + */ + @Get('/wallet/did') + @ApiTags('agent') + @UseGuards(AuthGuard('jwt'), RolesGuard) + @SetMetadata('permissions', [CommonConstants.PERMISSION_ORG_MGMT]) + @ApiQuery({ name: '_public', required: false }) + @ApiQuery({ name: 'verkey', required: false }) + @ApiQuery({ name: 'did', required: false }) + @ApiOperation({ summary: 'List of all DID', description: 'List of all the DID created for the current Cloud Agent.' }) + @ApiResponse({ status: 200, description: 'Success', type: ApiResponseDto }) + @ApiUnauthorizedResponse({ status: 401, description: 'Unauthorized', type: UnauthorizedErrorDto }) + @ApiForbiddenResponse({ status: 403, description: 'Forbidden', type: ForbiddenErrorDto }) + getAllDid( + @GetUser() user: any, + @Query('_public') _public: boolean, + @Query('verkey') verkey: string, + @Query('did') did: string + ): Promise { + this.logger.log(`**** Fetch all Did...`); + return this.agentService.getAllDid(_public, verkey, did, user); + } + + /** + * + * @param user + * @returns Created DID + */ + @Post('/wallet/did/create') + @ApiTags('agent') + @UseGuards(AuthGuard('jwt'), RolesGuard) + @SetMetadata('permissions', [CommonConstants.PERMISSION_ORG_MGMT]) + @ApiOperation({ summary: 'Create a new DID', description: 'Create a new did for the current Cloud Agent wallet.' }) + @ApiResponse({ status: 201, description: 'Success', type: ApiResponseDto }) + @ApiUnauthorizedResponse({ status: 401, description: 'Unauthorized', type: UnauthorizedErrorDto }) + @ApiForbiddenResponse({ status: 403, description: 'Forbidden', type: ForbiddenErrorDto }) + createLocalDid( + @GetUser() user: any + ): Promise { + this.logger.log(`**** Create Local Did...`); + return this.agentService.createLocalDid(user); + } + + /** + * + * @param walletUserDetails + * @param user + * @returns + */ + @Post('/wallet/provision') + @ApiTags('agent') + @UseGuards(AuthGuard('jwt'), RolesGuard) + @SetMetadata('permissions', [CommonConstants.PERMISSION_USER_MANAGEMENT]) + @ApiOperation({ + summary: 'Create wallet and start ACA-Py', + description: 'Create a new wallet and spin up your Aries Cloud Agent Python by selecting your desired network.' + }) + @ApiResponse({ status: 201, description: 'Success', type: ApiResponseDto }) + @ApiUnauthorizedResponse({ status: 401, description: 'Unauthorized', type: UnauthorizedErrorDto }) + @ApiForbiddenResponse({ status: 403, description: 'Forbidden', type: ForbiddenErrorDto }) + walletProvision( + @Body() walletUserDetails: WalletDetailsDto, + @GetUser() user: object + ): Promise { + this.logger.log(`**** Spin up the agent...${JSON.stringify(walletUserDetails)}`); + + const regex = new RegExp('^[a-zA-Z0-9]+$'); + if (!regex.test(walletUserDetails.walletName)) { + this.logger.error(`Wallet name in wrong format.`); + throw new BadRequestException(`Please enter valid wallet name, It allows only alphanumeric values`); + } + const decryptedPassword = this.commonService.decryptPassword(walletUserDetails.walletPassword); + walletUserDetails.walletPassword = decryptedPassword; + return this.agentService.walletProvision(walletUserDetails, user); + } + + /** + * Description: Route for fetch public DID + */ + @Get('/wallet/did/public') + @ApiTags('agent') + @UseGuards(AuthGuard('jwt'), RolesGuard) + @SetMetadata('permissions', [CommonConstants.PERMISSION_ORG_MGMT]) + @ApiOperation({ summary: 'Fetch the current public DID', description: 'Fetch the current public DID.' }) + @ApiResponse({ status: 200, description: 'Success', type: ApiResponseDto }) + @ApiUnauthorizedResponse({ status: 401, description: 'Unauthorized', type: UnauthorizedErrorDto }) + @ApiForbiddenResponse({ status: 403, description: 'Forbidden', type: ForbiddenErrorDto }) + getPublicDid( + @GetUser() user: any + ): Promise { + this.logger.log(`**** Fetch public Did...`); + return this.agentService.getPublicDid(user); + } + + /** + * Description: Route for assign public DID + * @param did + */ + @Get('/wallet/did/public/:id') + @ApiTags('agent') + @UseGuards(AuthGuard('jwt'), RolesGuard) + @SetMetadata('permissions', [CommonConstants.PERMISSION_USER_MANAGEMENT]) + @ApiOperation({ summary: 'Assign public DID', description: 'Assign public DID for the current use.' }) + @ApiResponse({ status: 200, description: 'Success', type: ApiResponseDto }) + @ApiUnauthorizedResponse({ status: 401, description: 'Unauthorized', type: UnauthorizedErrorDto }) + @ApiForbiddenResponse({ status: 403, description: 'Forbidden', type: ForbiddenErrorDto }) + assignPublicDid( + @Param('id') id: number, + @GetUser() user: any + ): Promise { + this.logger.log(`**** Assign public DID...`); + this.logger.log(`user: ${user.orgId} == id: ${Number(id)}`); + + if (user.orgId === Number(id)) { + return this.agentService.assignPublicDid(id, user); + } else { + this.logger.error(`Cannot make DID public of requested organization.`); + throw new BadRequestException(`Cannot make DID public requested organization.`); + } + } + + + /** + * Description: Route for onboarding register role on ledger + * @param role + * @param alias + * @param verkey + * @param did + */ + @Get('/ledger/register-nym/:id') + @ApiTags('agent') + @UseGuards(AuthGuard('jwt'), RolesGuard) + @SetMetadata('permissions', [CommonConstants.PERMISSION_ORG_MGMT]) + @ApiOperation({ summary: 'Send a NYM registration to the ledger', description: 'Write the DID to the ledger to make that DID public.' }) + @ApiResponse({ status: 200, description: 'Success', type: ApiResponseDto }) + @ApiUnauthorizedResponse({ status: 401, description: 'Unauthorized', type: UnauthorizedErrorDto }) + @ApiForbiddenResponse({ status: 403, description: 'Forbidden', type: ForbiddenErrorDto }) + registerNym( + @Param('id') id: number, + @GetUser() user: IUserRequestInterface + ): Promise { + this.logger.log(`user: ${typeof user.orgId} == id: ${typeof Number(id)}`); + + if (user.orgId !== Number(id)) { + return this.agentService.registerNym(id, user); + } else { + this.logger.error(`Cannot register nym of requested organization.`); + throw new BadRequestException(`Cannot register nym of requested organization`); + } + } + + @Get('/agents/:orgId/service/:action') + @ApiTags('platform-admin') + @UseGuards(AuthGuard('jwt'), RolesGuard) + @SetMetadata('permissions', [CommonConstants.PERMISSION_PLATFORM_MANAGEMENT]) + @ApiOperation({ + summary: 'Restart/Stop an running Aries Agent. (Platform Admin)', + description: 'Platform Admin can restart or stop the running Aries Agent. (Platform Admin)' + }) + @ApiResponse({ status: 200, description: 'Success', type: ApiResponseDto }) + @ApiUnauthorizedResponse({ status: 401, description: 'Unauthorized', type: UnauthorizedErrorDto }) + @ApiForbiddenResponse({ status: 403, description: 'Forbidden', type: ForbiddenErrorDto }) + @ApiParam({ name: 'action', enum: AgentActions }) + restartStopAgent(@Param('orgId') orgId: number, @Param('action') action: string): Promise { + return this.agentService.restartStopAgent(action, orgId); + } + + @Get('/server/status') + @ApiTags('platform-admin') + @UseGuards(AuthGuard('jwt'), RolesGuard) + @SetMetadata('permissions', [CommonConstants.PERMISSION_CONNECTIONS]) + @ApiOperation({ + summary: 'Fetch Aries Cloud Agent status', + description: 'Fetch the status of the Aries Cloud Agent.' + }) + @ApiResponse({ status: 200, description: 'Success', type: ApiResponseDto }) + @ApiUnauthorizedResponse({ status: 401, description: 'Unauthorized', type: UnauthorizedErrorDto }) + @ApiForbiddenResponse({ status: 403, description: 'Forbidden', type: ForbiddenErrorDto }) + getAgentServerStatus(@GetUser() user: any): Promise { + this.logger.log(`**** getPlatformConfig called...`); + return this.agentService.getAgentServerStatus(user); + } + + @Get('/ping-agent') + @UseGuards(AuthGuard('jwt')) + @ApiTags('service-status') + @ApiExcludeEndpoint() + @ApiResponse({ + status: 200, + description: 'The agent service status' + }) + pingServiceAgent(): Promise { + this.logger.log(`**** pingServiceAgent called`); + return this.agentService.pingServiceAgent(); + } + + @Get('/spinup-status') + @ApiTags('platform-admin') + @UseGuards(AuthGuard('jwt'), RolesGuard) + @SetMetadata('permissions', [CommonConstants.PERMISSION_ORG_MGMT]) + @ApiOperation({ + summary: 'List all Aries Cloud Agent status', + description: 'List of all created Aries Cloud Agent status.' + }) + @ApiResponse({ status: 200, description: 'Success', type: ApiResponseDto }) + @ApiUnauthorizedResponse({ status: 401, description: 'Unauthorized', type: UnauthorizedErrorDto }) + @ApiForbiddenResponse({ status: 403, description: 'Forbidden', type: ForbiddenErrorDto }) + @ApiQuery({ name: 'items_per_page', required: false }) + @ApiQuery({ name: 'page', required: false }) + @ApiQuery({ name: 'search_text', required: false }) + @ApiQuery({ name: 'status', required: false }) + @ApiQuery({ name: 'sortValue', enum: sortValue, required: false }) + @ApiQuery({ name: 'status', enum: booleanStatus, required: false }) + agentSpinupStatus( + @Query('items_per_page') items_per_page: number, + @Query('page') page: number, + @Query('search_text') search_text: string, + @Query('sortValue') sortValue: any, + @Query('status') status: any, + @GetUser() user: any + ): Promise { + + this.logger.log(`status: ${typeof status} ${status}`); + + items_per_page = items_per_page || 10; + page = page || 1; + search_text = search_text || ''; + sortValue = sortValue ? sortValue : 'DESC'; + status = status ? status : 'all'; + + let agentsStatus: any; + if ('all' === status) { + agentsStatus = 3; + } else if ('true' === status) { + agentsStatus = 2; + } else if ('false' === status) { + agentsStatus = 1; + } else { + throw new BadRequestException('Invalid status received'); + } + + this.logger.log(`**** agentSpinupStatus called`); + return this.agentService.agentSpinupStatus(items_per_page, page, search_text, agentsStatus, sortValue, user); + } +} diff --git a/apps/api-gateway/src/agent/agent.module.ts b/apps/api-gateway/src/agent/agent.module.ts new file mode 100644 index 000000000..224670431 --- /dev/null +++ b/apps/api-gateway/src/agent/agent.module.ts @@ -0,0 +1,26 @@ +import { Module } from '@nestjs/common'; +import { HttpModule } from '@nestjs/axios'; +import { AgentController } from './agent.controller'; +import { AgentService } from './agent.service'; +import { ClientsModule } from '@nestjs/microservices'; +import { CommonModule } from '../../../../libs/common/src/common.module'; +import { CommonService } from '../../../../libs/common/src/common.service'; +import { ConfigModule } from '@nestjs/config'; +import { commonNatsOptions } from 'libs/service/nats.options'; + +@Module({ + imports: [ + HttpModule, + ConfigModule.forRoot(), + ClientsModule.register([ + { + name: 'NATS_CLIENT', + ...commonNatsOptions('AGENT_SERVICE:REQUESTER') + }, + CommonModule + ]) + ], + controllers: [AgentController], + providers: [AgentService, CommonService] +}) +export class AgentModule { } diff --git a/apps/api-gateway/src/agent/agent.service.spec.ts b/apps/api-gateway/src/agent/agent.service.spec.ts new file mode 100644 index 000000000..2b7709ccc --- /dev/null +++ b/apps/api-gateway/src/agent/agent.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AgentService } from './agent.service'; + +describe('AgentService', () => { + let service: AgentService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [AgentService] + }).compile(); + + service = module.get(AgentService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/apps/api-gateway/src/agent/agent.service.ts b/apps/api-gateway/src/agent/agent.service.ts new file mode 100644 index 000000000..3dae70b77 --- /dev/null +++ b/apps/api-gateway/src/agent/agent.service.ts @@ -0,0 +1,95 @@ +import { Injectable, Logger, Inject, HttpException } from '@nestjs/common'; +import { ClientProxy } from '@nestjs/microservices'; +import { BaseService } from 'libs/service/base.service'; +import { map } from 'rxjs/operators'; +import { WalletDetailsDto } from '../dtos/wallet-details.dto'; + +@Injectable() +export class AgentService extends BaseService { + constructor( + @Inject('NATS_CLIENT') private readonly agentServiceProxy: ClientProxy + ) { + super('AgentService'); + } + + + /** + * Description: Calling agent service for get-all-did + * @param _public + * @param verkey + * @param did + */ + getAllDid(_public: boolean, verkey: string, did: string, user: any) { + this.logger.log('**** getAllDid called...'); + const payload = { _public, verkey, did, user }; + return this.sendNats(this.agentServiceProxy, 'get-all-did', payload); + } + + /** + * Description: Calling agent service for create-local-did + */ + createLocalDid(user: any) { + this.logger.log('**** createLocalDid called...'); + return this.sendNats(this.agentServiceProxy, 'create-local-did', user); + } + + async walletProvision(walletUserDetails: WalletDetailsDto, user: any) { + this.logger.log(`**** walletProvision called...${JSON.stringify(walletUserDetails)}`); + const payload = { walletUserDetails, user }; + return await this.sendNats(this.agentServiceProxy, 'wallet-provision', payload); + } + + /** + * Description: Calling agent service for get-public-did + */ + getPublicDid(user: any) { + this.logger.log('**** getPublicDid called...'); + return this.sendNats(this.agentServiceProxy, 'get-public-did', user); + } + + /** + * Description: Calling agent service for assign-public-did + * @param did + */ + assignPublicDid(id: number, user: any) { + this.logger.log('**** assignPublicDid called...'); + const payload = { id, user }; + return this.sendNats(this.agentServiceProxy, 'assign-public-did-org', payload); + } + + + /** + * Description: Calling agent service for onboard-register-ledger + * @param role + * @param alias + * @param verkey + * @param did + */ + registerNym(id: number, user: any) { + this.logger.log('**** registerNym called...'); + const payload = { id, user }; + return this.sendNats(this.agentServiceProxy, 'register-nym-org', payload); + } + + restartStopAgent(action: string, orgId: number) { + const payload = { action, orgId }; + return this.sendNats(this.agentServiceProxy, 'restart-stop-agent', payload); + } + + getAgentServerStatus(user) { + + return this.sendNats(this.agentServiceProxy, 'get-agent-server-status', user); + } + + pingServiceAgent() { + this.logger.log('**** pingServiceAgent called...'); + const payload = {}; + return this.sendNats(this.agentServiceProxy, 'ping-agent', payload); + } + + agentSpinupStatus(items_per_page: number, page: number, search_text: string, agentStatus: string, sortValue: string, user: any) { + this.logger.log('**** agentSpinupStatus called...'); + const payload = { items_per_page, page, search_text, agentStatus, sortValue, user }; + return this.sendNats(this.agentServiceProxy, 'get-agent-spinup-status', payload); + } +} diff --git a/apps/api-gateway/src/app.controller.spec.ts b/apps/api-gateway/src/app.controller.spec.ts new file mode 100644 index 000000000..a37961341 --- /dev/null +++ b/apps/api-gateway/src/app.controller.spec.ts @@ -0,0 +1,17 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AppController } from './app.controller'; +import { AppService } from './app.service'; + +describe('AppController', () => { + let appController: AppController; + + beforeEach(async () => { + const app: TestingModule = await Test.createTestingModule({ + controllers: [AppController], + providers: [AppService] + }).compile(); + + appController = app.get(AppController); + }); + +}); diff --git a/apps/api-gateway/src/app.controller.ts b/apps/api-gateway/src/app.controller.ts new file mode 100644 index 000000000..a778b44ba --- /dev/null +++ b/apps/api-gateway/src/app.controller.ts @@ -0,0 +1,10 @@ +import { Controller, Logger } from '@nestjs/common'; +import { ApiBearerAuth } from '@nestjs/swagger'; +import { AppService } from './app.service'; +@Controller() +@ApiBearerAuth() +export class AppController { + constructor(private readonly appService: AppService) {} + + private readonly logger = new Logger('AppController'); +} diff --git a/apps/api-gateway/src/app.module.ts b/apps/api-gateway/src/app.module.ts new file mode 100644 index 000000000..d710b05fb --- /dev/null +++ b/apps/api-gateway/src/app.module.ts @@ -0,0 +1,87 @@ +import { MiddlewareConsumer, Module, RequestMethod } from '@nestjs/common'; +import { AgentController } from './agent/agent.controller'; +import { AgentModule } from './agent-service/agent-service.module'; +import { AppController } from './app.controller'; +import { AppService } from './app.service'; +import { AuthzMiddleware } from './authz/authz.middleware'; +import { AuthzModule } from './authz/authz.module'; +import { ClientsModule } from '@nestjs/microservices'; +import { ConfigModule } from '@nestjs/config'; +import { CredentialDefinitionModule } from './credential-definition/credential-definition.module'; +import { FidoModule } from './fido/fido.module'; +import { IssuanceModule } from './issuance/issuance.module'; +import { OrganizationModule } from './organization/organization.module'; +import { PlatformController } from './platform/platform.controller'; +import { PlatformModule } from './platform/platform.module'; +import { VerificationModule } from './verification/verification.module'; +import { RevocationController } from './revocation/revocation.controller'; +import { RevocationModule } from './revocation/revocation.module'; +import { SchemaModule } from './schema/schema.module'; +import { commonNatsOptions } from 'libs/service/nats.options'; +import { UserModule } from './user/user.module'; +import { ConnectionModule } from './connection/connection.module'; + +@Module({ + imports: [ + ConfigModule.forRoot(), + ClientsModule.register([ + { + name: 'NATS_CLIENT', + ...commonNatsOptions('AGENT_SERVICE:REQUESTER') + } + ]), + AgentModule, + PlatformModule, + AuthzModule, + CredentialDefinitionModule, + SchemaModule, + RevocationModule, + VerificationModule, + FidoModule, + OrganizationModule, + UserModule, + ConnectionModule, + IssuanceModule + ], + controllers: [AppController], + providers: [AppService] +}) +export class AppModule { + configure(userContext: MiddlewareConsumer): void { + userContext.apply(AuthzMiddleware) + .exclude({ path: 'authz', method: RequestMethod.ALL }, + 'authz/:splat*', + 'admin/subscriptions', + 'registry/organizations/', + 'email/user/verify', + 'platform/connection', + 'platform/test', + 'category/active-categories', + 'credential-definition/holder/:orgId', + 'admin/organizations', + 'admin/forgot-password', + 'present-proof/holder-remote/credential-record', + 'present-proof/record/verifier-remote/:verifierId', + 'registry/test', + 'admin/check-user-exist/:username', + 'admin/org-name-exists', + 'admin/user-email-exists/:email', + 'registry/organizations/invitations', + 'tenants/:id', + 'tenants', + 'tenants/invitations/:id', + 'admin/user-by-email/:email', + 'registry/update-user-using-invitation', + 'present-proof/generate-proof-request', + 'admin/user-login', + 'registry/organizations', + 'issue-credentials/national-id', + 'labels/:id' + ) + .forRoutes( + AgentController, + PlatformController, + RevocationController + ); + } +} diff --git a/apps/api-gateway/src/app.service.ts b/apps/api-gateway/src/app.service.ts new file mode 100644 index 000000000..095adb165 --- /dev/null +++ b/apps/api-gateway/src/app.service.ts @@ -0,0 +1,12 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { ClientProxy } from '@nestjs/microservices'; +import { BaseService } from '../../../libs/service/base.service'; + +@Injectable() +export class AppService extends BaseService { + constructor( + @Inject('NATS_CLIENT') private readonly appServiceProxy: ClientProxy + ) { + super('appService'); + } +} diff --git a/apps/api-gateway/src/authz/authz.controller.ts b/apps/api-gateway/src/authz/authz.controller.ts new file mode 100644 index 000000000..63b26cfa5 --- /dev/null +++ b/apps/api-gateway/src/authz/authz.controller.ts @@ -0,0 +1,17 @@ +import { + Controller, + Logger +} from '@nestjs/common'; +import { AuthzService } from './authz.service'; +// import { CommonService } from "@credebl/common"; +import { CommonService } from '../../../../libs/common/src/common.service'; + + +@Controller('authz') +export class AuthzController { + private logger = new Logger('AuthzController'); + + constructor(private readonly authzService: AuthzService, + private readonly commonService: CommonService) { } + +} diff --git a/apps/api-gateway/src/authz/authz.middleware.ts b/apps/api-gateway/src/authz/authz.middleware.ts new file mode 100644 index 000000000..fd961a937 --- /dev/null +++ b/apps/api-gateway/src/authz/authz.middleware.ts @@ -0,0 +1,131 @@ +/* eslint-disable camelcase */ +import { + HttpException, + Injectable, + Logger, + NestMiddleware, + UnauthorizedException +} from '@nestjs/common'; + +import { AuthzService } from './authz.service'; +import { CommonConstants } from '@credebl/common/common.constant'; +import { ExtractJwt } from 'passport-jwt'; +import { JwtService } from '@nestjs/jwt'; +import { NextFunction } from 'express'; +import { RequestingUser } from './dtos/requesting-user.dto'; + +@Injectable() +export class AuthzMiddleware implements NestMiddleware { + constructor(private readonly authService: AuthzService) { } + private readonly logger = new Logger('AuthzMiddleware'); + + /** + * Decodes and extracts the payload from the token + * + * @param token The authorization bearer token + * + * @throws UnauthorizedException If the token is not found + */ + getPayload = (token: string): unknown => { + + if (!token) { + throw new UnauthorizedException( + 'Authorization header does not contain a token' + ); + } + + // ignore options since we don't need to verify here + const jwtService = new JwtService({}); + const decoded = jwtService.decode(token, { complete: true }); + + if (!decoded) { + throw new UnauthorizedException( + 'Authorization header contains an invalid token' + ); + } + + return decoded['payload']; + }; + + async use(req: Request, res: Response, next: NextFunction): Promise { + // get token and decode or any custom auth logic + try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const token = ExtractJwt.fromAuthHeaderAsBearerToken()(req as any); + let payload; + + try { + payload = this.getPayload(token); + } catch (error) { + this.logger.log(`Caught error while parsing payload: ${error}`); + next(error); + return; + } + + const requestor = new RequestingUser(); + const tenant = (await this.authService.getUserByKeycloakUserId(payload['sub']))?.response; + + if (tenant) { + this.logger.log(`tenant this.authService.getUserByKeycloakUserId: ${tenant.keycloakUserId}`); + this.logger.log(`tenant id: ${tenant.id}`); + + requestor.tenant_name = `${tenant.firstName} ${tenant.lastName}`; + requestor.tenant_id = tenant.id; + requestor.userRoleOrgPermissions = tenant.userRoleOrgMap; + requestor.orgId = tenant.userRoleOrgMap[0].organization.id; + requestor.apiKey = tenant.userRoleOrgMap[0].organization.apiKey; + requestor.agentEndPoint = tenant.userRoleOrgMap[0].organization.agentEndPoint; + + let tenantOrgInfo; + + for (const item of tenant.userRoleOrgMap) { + this.logger.log(`${JSON.stringify(item.organization.orgRole)}`); + + if (item.organization.orgRole.id == CommonConstants.ORG_TENANT_ROLE) { + this.logger.log(`In Tenant Org matched id : ${item.organization.id}`); + tenantOrgInfo = item.organization; + + } + } + + if (null != tenantOrgInfo) { + requestor.tenantOrgId = tenantOrgInfo.id; + } + + if (payload.hasOwnProperty('clientId')) { + this.logger.log(`tenant requestor.permissions: ${JSON.stringify(requestor)}`); + } else { + requestor.email = payload['email']; + + const userData + = ( + await this.authService.getUserByKeycloakUserId(payload['sub']) + )?.response; + + this.logger.debug(`User by keycloak ID ${userData.id}`); + + requestor.userId = userData?.id; + requestor.name = `${userData.firstName} ${userData.lastName}`; + + if (null != userData?.organization) { + this.logger.log(`Org Not Null: ${userData?.organization.Id} `); + requestor.orgId = userData?.organization.id; + } + + this.logger.log(` user id ${userData.id}`); + } + } + + req['requestor'] = requestor; + + next(); + } catch (error) { + this.logger.error( + `RequestorMiddleware Error in middleware: ${error} ${JSON.stringify( + error + )}` + ); + next(new HttpException(error, 500)); + } + } +} diff --git a/apps/api-gateway/src/authz/authz.module.ts b/apps/api-gateway/src/authz/authz.module.ts new file mode 100644 index 000000000..b8e84d2ee --- /dev/null +++ b/apps/api-gateway/src/authz/authz.module.ts @@ -0,0 +1,57 @@ +import { ClientsModule, Transport } from '@nestjs/microservices'; + +import { AgentService } from '../agent/agent.service'; +import { AuthzController } from './authz.controller'; +import { AuthzService } from './authz.service'; +import { CommonModule } from '../../../../libs/common/src/common.module'; +import { CommonService } from '../../../../libs/common/src/common.service'; +import { ConnectionService } from '../connection/connection.service'; +import { HttpModule } from '@nestjs/axios'; +import { JwtStrategy } from './jwt.strategy'; +import { MobileJwtStrategy } from './mobile-jwt.strategy'; +import { Module } from '@nestjs/common'; +import { PassportModule } from '@nestjs/passport'; +import { VerificationService } from '../verification/verification.service'; +import { SocketGateway } from './socket.gateway'; +import { UserModule } from '../user/user.module'; +import { UserService } from '../user/user.service'; + +//import { WebhookService } from "../../../platform-service/src/webhook/webhook.service"; + +@Module({ + imports: [ + HttpModule, + PassportModule.register({ + defaultStrategy: 'jwt', + mobileStrategy: 'mobile-jwt' + }), + ClientsModule.register([ + { + name: 'NATS_CLIENT', + transport: Transport.NATS, + options: { + servers: [`${process.env.NATS_URL}`] + } + }, + CommonModule + ]), + UserModule + ], + providers: [ + JwtStrategy, + AuthzService, + MobileJwtStrategy, + SocketGateway, + VerificationService, + ConnectionService, + AgentService, + CommonService, + UserService + ], + exports: [ + PassportModule, + AuthzService + ], + controllers: [AuthzController] +}) +export class AuthzModule { } \ 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 new file mode 100644 index 000000000..ab5cd587c --- /dev/null +++ b/apps/api-gateway/src/authz/authz.service.ts @@ -0,0 +1,28 @@ +import { Injectable, Inject } from '@nestjs/common'; +import { ClientProxy } from '@nestjs/microservices'; +import { BaseService } from '../../../../libs/service/base.service'; +import { + WebSocketGateway, + WebSocketServer + +} from '@nestjs/websockets'; + + +@Injectable() +@WebSocketGateway() +export class AuthzService extends BaseService { + //private logger = new Logger('AuthService'); + @WebSocketServer() server; + constructor( + @Inject('NATS_CLIENT') private readonly authServiceProxy: ClientProxy + ) { + + super('AuthzService'); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + getUserByKeycloakUserId(keycloakUserId: string): Promise { + return this.sendNats(this.authServiceProxy, 'get-user-by-keycloakUserId', keycloakUserId); + } + +} diff --git a/apps/api-gateway/src/authz/decorators/get-user.decorator.ts b/apps/api-gateway/src/authz/decorators/get-user.decorator.ts new file mode 100644 index 000000000..05a6151ed --- /dev/null +++ b/apps/api-gateway/src/authz/decorators/get-user.decorator.ts @@ -0,0 +1,7 @@ +import { createParamDecorator, ExecutionContext } from '@nestjs/common'; +import { RequestingUser } from '../dtos/requesting-user.dto'; + +export const GetUser = createParamDecorator((data, ctx: ExecutionContext): RequestingUser => { + const req = ctx.switchToHttp().getRequest(); + return req.requestor; +}); diff --git a/apps/api-gateway/src/authz/decorators/roles.decorator.ts b/apps/api-gateway/src/authz/decorators/roles.decorator.ts new file mode 100644 index 000000000..a172bdaee --- /dev/null +++ b/apps/api-gateway/src/authz/decorators/roles.decorator.ts @@ -0,0 +1,9 @@ +import { CustomDecorator } from '@nestjs/common'; +import { OrgRoles } from 'libs/org-roles/enums'; +import { SetMetadata } from '@nestjs/common'; + +export const ROLES_KEY = 'roles'; +export const Roles = (...roles: OrgRoles[]): CustomDecorator => SetMetadata(ROLES_KEY, roles); +export const Permissions = (...permissions: string[]): CustomDecorator => SetMetadata('permissions', permissions); +export const Subscriptions = (...subscriptions: string[]): CustomDecorator => SetMetadata('subscriptions', subscriptions); + diff --git a/apps/api-gateway/src/authz/decorators/user.decorator.ts b/apps/api-gateway/src/authz/decorators/user.decorator.ts new file mode 100644 index 000000000..c733df0e8 --- /dev/null +++ b/apps/api-gateway/src/authz/decorators/user.decorator.ts @@ -0,0 +1,8 @@ +import { createParamDecorator, ExecutionContext } from '@nestjs/common'; + +export const User = createParamDecorator( + (data: unknown, ctx: ExecutionContext) => { + const request = ctx.switchToHttp().getRequest(); + return request.user; + } +); \ No newline at end of file diff --git a/apps/api-gateway/src/authz/dtos/auth-token-res.dto.ts b/apps/api-gateway/src/authz/dtos/auth-token-res.dto.ts new file mode 100644 index 000000000..7c7e7a642 --- /dev/null +++ b/apps/api-gateway/src/authz/dtos/auth-token-res.dto.ts @@ -0,0 +1,21 @@ +import { ApiResponseProperty } from '@nestjs/swagger'; + +export class AuthTokenResponse { + + @ApiResponseProperty({ example: 'eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI4aExRb0lqeHRvTDBFVk9kNTJiZzNpbWt5cEt5SnNHSU5rTEd3VmQzWkdvIn0.eyJleHAiOjE2MTU1NDE1NzcsImlhdCI6MTYxNTU0MTI3NywianRpIjoiMjczOTJiYzctODNmNC00Yzg0LWJiODQtOTA0NjJmMTMyMWVkIiwiaXNzIjoiaHR0cDovLzM1LjE4OC44MC4zMjo4MDgwL2F1dGgvcmVhbG1zL2NyZWRlYmwtcGxhdGZvcm0iLCJhdWQiOlsiYWRtaW4tQ2FiaW0iLCJhZG1pbi1DZWxsbyIsImFkbWluLXp1bnV4ZSIsImFkbWluLUF5YW5Xb3JrcyBUZWNobm9sb2d5IFNvbHV0aW9ucyBQdnQuIEx0ZC4iLCJhZG1pbi1zZXJvcGFzcyIsImFkbWluLUhQRUMiLCJhZG1pbi1WeXZpYyIsImFkbWluLVNoaW5jaGFuIFRlY2hub2xvZ2llcyIsImFkbWluLVJva293eXIiLCJhZG1pbi1heWFud29ya3MiLCJhZG1pbi1UeXJvY2FyZSIsImFkbWluLVRlbmFudEJhbmsiLCJhZG1pbi1jZWZvcyIsImFkbWluLWNlbG90b3MiLCJhY2NvdW50IiwiYWRtaW4tY2Fub24iXSwic3ViIjoiZDhiOWI4MWItNzIzMi00YTdmLWI5ZDMtNGY3MjI2MTI5Njc3IiwidHlwIjoiQmVhcmVyIiwiYXpwIjoiYWRleWFDbGllbnQiLCJhY3IiOiIxIiwicmVzb3VyY2VfYWNjZXNzIjp7ImFkbWluLUNhYmltIjp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50Iiwidmlldy1wcm9maWxlIl19LCJhZG1pbi1DZWxsbyI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsInZpZXctcHJvZmlsZSJdfSwiYWRtaW4tenVudXhlIjp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50Iiwidmlldy1wcm9maWxlIl19LCJhZG1pbi1BeWFuV29ya3MgVGVjaG5vbG9neSBTb2x1dGlvbnMgUHZ0LiBMdGQuIjp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50Iiwidmlldy1wcm9maWxlIl19LCJhZG1pbi1zZXJvcGFzcyI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsInZpZXctcHJvZmlsZSJdfSwiYWRtaW4tSFBFQyI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsInZpZXctcHJvZmlsZSJdfSwiYWRtaW4tVnl2aWMiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJ2aWV3LXByb2ZpbGUiXX0sImFkbWluLVNoaW5jaGFuIFRlY2hub2xvZ2llcyI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsInZpZXctcHJvZmlsZSJdfSwiYWRtaW4tUm9rb3d5ciI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsInZpZXctcHJvZmlsZSJdfSwiYWRtaW4tYXlhbndvcmtzIjp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50Iiwidmlldy1wcm9maWxlIl19LCJhZG1pbi1UeXJvY2FyZSI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsInZpZXctcHJvZmlsZSJdfSwiYWRtaW4tVGVuYW50QmFuayI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsInZpZXctcHJvZmlsZSJdfSwiYWRtaW4tY2Vmb3MiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJ2aWV3LXByb2ZpbGUiXX0sImFkbWluLWNlbG90b3MiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJ2aWV3LXByb2ZpbGUiXX0sImFjY291bnQiOnsicm9sZXMiOlsidmlldy1wcm9maWxlIl19LCJhZG1pbi1jYW5vbiI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoiZW1haWwgcHJvZmlsZSIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwiY2xpZW50SWQiOiJhZGV5YUNsaWVudCIsImNsaWVudEhvc3QiOiIxMjQuNjYuMTcwLjExMCIsInByZWZlcnJlZF91c2VybmFtZSI6InNlcnZpY2UtYWNjb3VudC1hZGV5YWNsaWVudCIsImNsaWVudEFkZHJlc3MiOiIxMjQuNjYuMTcwLjExMCJ9.XY_m5rZuqK6AvIhuz6VDjVgTz4iC6SadEj-BmfhiEWQxhMyOcvRosZGIPOv4ywE_5Xzs0FUygJ0NMGjSddHYAY-jNMicWa6mw7bqZzit-5VZK7ClEpU_8QMqWy9bkobXwmNJpQc4pdbqPK8oKXak7U95LkvCNil1j3SFSVfVzctIAjA9Pw0jSdcamNlHQE4EQ1gTv6G3VdJEsZ4mrghR13oODUf80yqogsoXv5BRs9-FZhORAhzzSD5Qn6q7ZgEE8N7jXXmtg4dPCzu5mCCgDbLEd3T-IBqP7DlzlAmE_TS2U7jbaz83R8Xvg3iMAvJt9ETXgG9373b2QM0xHwLRsQ' }) + // tslint:disable-next-line: variable-name + access_token: string; + + @ApiResponseProperty({ example: 'email profile' }) + scope: string; + + @ApiResponseProperty({ example: 86400 }) + // tslint:disable-next-line: variable-name + expires_in: number; + + @ApiResponseProperty({ example: 'Bearer' }) + // tslint:disable-next-line: variable-name + token_type: string; + + +} diff --git a/apps/api-gateway/src/authz/dtos/client-login.dto.ts b/apps/api-gateway/src/authz/dtos/client-login.dto.ts new file mode 100644 index 000000000..41d40b578 --- /dev/null +++ b/apps/api-gateway/src/authz/dtos/client-login.dto.ts @@ -0,0 +1,10 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class ClientLoginDto { + @ApiProperty() + clientId: string; + + @ApiProperty() + clientSecret: string; + +} diff --git a/apps/api-gateway/src/authz/dtos/firebase-token.dto.ts b/apps/api-gateway/src/authz/dtos/firebase-token.dto.ts new file mode 100644 index 000000000..8d04ccc32 --- /dev/null +++ b/apps/api-gateway/src/authz/dtos/firebase-token.dto.ts @@ -0,0 +1,8 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsString, IsNotEmpty } from 'class-validator'; +export class FirebaseTokenDto { + @ApiProperty() + @IsNotEmpty({message:'Please provide valid firebaseToken'}) + @IsString({message:'FirebaseToken should be string'}) + firebaseToken: string; +} \ No newline at end of file diff --git a/apps/api-gateway/src/authz/dtos/requesting-user.dto.ts b/apps/api-gateway/src/authz/dtos/requesting-user.dto.ts new file mode 100644 index 000000000..f4bf22f34 --- /dev/null +++ b/apps/api-gateway/src/authz/dtos/requesting-user.dto.ts @@ -0,0 +1,20 @@ +/* eslint-disable camelcase */ +import { UserRoleOrgPermsDto } from './user-role-org-perms.dto'; + +export class RequestingUser { + userId: number; + username: string; + //roleId: number; + email: string; + //permissions: string[]; + orgId: number; + //org?: OrganizationDto; + name?: string; + agentEndPoint?: string; + apiKey?: string; + tenant_id?: number; + tenant_name?: string; + userRoleOrgPermissions: UserRoleOrgPermsDto[]; + tenantOrgId?: number; +} + diff --git a/apps/api-gateway/src/authz/dtos/user-login.dto.ts b/apps/api-gateway/src/authz/dtos/user-login.dto.ts new file mode 100644 index 000000000..bbbe129b3 --- /dev/null +++ b/apps/api-gateway/src/authz/dtos/user-login.dto.ts @@ -0,0 +1,12 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class UserLoginDto { + @ApiProperty() + username: string; + + @ApiProperty() + password: string; + + @ApiProperty() + firebaseToken: string; +} diff --git a/apps/api-gateway/src/authz/dtos/user-role-org-perms.dto.ts b/apps/api-gateway/src/authz/dtos/user-role-org-perms.dto.ts new file mode 100644 index 000000000..66d1bd86b --- /dev/null +++ b/apps/api-gateway/src/authz/dtos/user-role-org-perms.dto.ts @@ -0,0 +1,19 @@ + +export class UserRoleOrgPermsDto { + id :number; + role : userRoleDto; + Organization: userOrgDto; +} + +export class userRoleDto { + id: number; + name : string; + permissions :string[]; + +} + +export class userOrgDto { + id: number; + orgName :string; +} + diff --git a/apps/api-gateway/src/authz/guards/org-roles.guard.ts b/apps/api-gateway/src/authz/guards/org-roles.guard.ts new file mode 100644 index 000000000..0c6dd349e --- /dev/null +++ b/apps/api-gateway/src/authz/guards/org-roles.guard.ts @@ -0,0 +1,55 @@ +import { CanActivate, ExecutionContext, 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'; +import { Reflector } from '@nestjs/core'; + +@Injectable() +export class OrgRolesGuard implements CanActivate { + constructor(private reflector: Reflector) { } // eslint-disable-next-line array-callback-return + + + private logger = new Logger('Org Role Guard'); + async canActivate(context: ExecutionContext): Promise { + const requiredRoles = this.reflector.getAllAndOverride(ROLES_KEY, [ + context.getHandler(), + context.getClass() + ]); + const requiredRolesNames = Object.values(requiredRoles) as string[]; + + if (!requiredRolesNames) { + return true; + } + + // Request requires org check, proceed with it + const req = context.switchToHttp().getRequest(); + + const { user } = req; + + if (req.query.orgId || req.body.orgId) { + const orgId = req.query.orgId || req.body.orgId; + + const specificOrg = user.userOrgRoles.find((orgDetails) => { + if (!orgDetails.orgId) { + return false; + } + return orgDetails.orgId.toString() === orgId.toString(); + }); + + if (!specificOrg) { + throw new HttpException('Organization does not match', HttpStatus.FORBIDDEN); + } + + user.selectedOrg = specificOrg; + user.selectedOrg.orgRoles = user.userOrgRoles.map(roleItem => roleItem.orgRole.name); + + } else { + throw new HttpException('organization is required', HttpStatus.BAD_REQUEST); + } + + return requiredRoles.some((role) => user.selectedOrg?.orgRoles.includes(role)); + } +} diff --git a/apps/api-gateway/src/authz/jwt-payload.interface.ts b/apps/api-gateway/src/authz/jwt-payload.interface.ts new file mode 100644 index 000000000..0cdf673df --- /dev/null +++ b/apps/api-gateway/src/authz/jwt-payload.interface.ts @@ -0,0 +1,12 @@ +export interface JwtPayload { + iss: string; + sub: string; + aud: string[]; + iat?: number; + exp?: number; + azp: string; + scope: string; + gty?: string; + permissions: string[]; + } + \ No newline at end of file diff --git a/apps/api-gateway/src/authz/jwt.strategy.ts b/apps/api-gateway/src/authz/jwt.strategy.ts new file mode 100644 index 000000000..a53a78e07 --- /dev/null +++ b/apps/api-gateway/src/authz/jwt.strategy.ts @@ -0,0 +1,60 @@ +// src/authz/jwt.strategy.ts + +import * as dotenv from 'dotenv'; +import * as jwt from 'jsonwebtoken'; + +import { ExtractJwt, Strategy } from 'passport-jwt'; +import { Injectable, Logger } from '@nestjs/common'; + +import { CommonConstants } from '@credebl/common/common.constant'; +import { JwtPayload } from './jwt-payload.interface'; +import { NotFoundException } from '@nestjs/common'; +import { PassportStrategy } from '@nestjs/passport'; +import { UserService } from '../user/user.service'; +import { passportJwtSecret } from 'jwks-rsa'; + +dotenv.config(); + +@Injectable() +export class JwtStrategy extends PassportStrategy(Strategy) { + private readonly logger = new Logger(); + + constructor( + private readonly usersService: UserService + ) { + super({ + + secretOrKeyProvider: (request, jwtToken, done) => { + const decodedToken = jwt.decode(jwtToken) as jwt.JwtPayload; + const audiance = decodedToken.iss.toString(); + const jwtOptions = { + cache: true, + rateLimit: true, + jwksRequestsPerMinute: 5, + jwksUri: `${audiance}${CommonConstants.URL_KEYCLOAK_JWKS}` + }; + const secretprovider = passportJwtSecret(jwtOptions); + let certkey; + secretprovider(request, jwtToken, async (err, data) => { + certkey = data; + done(null, certkey); + }); + }, + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + algorithms: ['RS256'] + }); + } + async validate(payload: JwtPayload): Promise { + + const userDetails = await this.usersService.findUserByKeycloakId(payload?.sub); + + if (!userDetails.response) { + throw new NotFoundException('Keycloak user not found'); + } + + return { + ...userDetails.response, + ...payload + }; + } +} diff --git a/apps/api-gateway/src/authz/mobile-jwt.strategy.ts b/apps/api-gateway/src/authz/mobile-jwt.strategy.ts new file mode 100644 index 000000000..be5b49fc7 --- /dev/null +++ b/apps/api-gateway/src/authz/mobile-jwt.strategy.ts @@ -0,0 +1,51 @@ +import * as dotenv from 'dotenv'; +import * as jwt from 'jsonwebtoken'; + +import { ExtractJwt, Strategy } from 'passport-jwt'; +import { BadRequestException, Injectable, Logger, UnauthorizedException } from '@nestjs/common'; + +import { CommonConstants } from '@credebl/common/common.constant'; +import { PassportStrategy } from '@nestjs/passport'; +import { passportJwtSecret } from 'jwks-rsa'; +dotenv.config(); +const logger = new Logger(); + +@Injectable() +export class MobileJwtStrategy extends PassportStrategy(Strategy, 'mobile-jwt') { + private readonly logger = new Logger(); + + constructor() { + super({ + + secretOrKeyProvider: (request, jwtToken, done) => { + const decodedToken: any = jwt.decode(jwtToken); + const audiance = decodedToken.iss.toString(); + const jwtOptions = { + cache: true, + rateLimit: true, + jwksRequestsPerMinute: 5, + jwksUri: `${audiance}${CommonConstants.URL_KEYCLOAK_JWKS}` + }; + const secretprovider = passportJwtSecret(jwtOptions); + let certkey; + secretprovider(request, jwtToken, async (err, data) => { + certkey = data; + done(null, certkey); + }); + }, + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + algorithms: ['RS256'] + }); + } + + validate(payload: any) { + if ('adeyaClient' !== payload.azp) { + throw new UnauthorizedException( + 'Authorization header contains an invalid token' + ); + } else { + return payload; + } + + } +} diff --git a/apps/api-gateway/src/authz/roles.guard.ts b/apps/api-gateway/src/authz/roles.guard.ts new file mode 100644 index 000000000..a79993757 --- /dev/null +++ b/apps/api-gateway/src/authz/roles.guard.ts @@ -0,0 +1,55 @@ +import { CanActivate, ExecutionContext, Injectable, Logger } from '@nestjs/common'; + +import { Reflector } from '@nestjs/core'; + +@Injectable() +export class RolesGuard implements CanActivate { + constructor(private reflector: Reflector) { } + + private readonly logger = new Logger('RolesGuard'); + + async canActivate(context: ExecutionContext): Promise { + this.logger.log(`Before permissions`); + const permissions = this.reflector.get('permissions', context.getHandler()); + this.logger.log(`permissions: ${permissions}`); + + if (!permissions) { + this.logger.log(`No Permissions found.`); + return true; + } + + const subscription = this.reflector.get('subscription', context.getHandler()); + this.logger.log(`subscription: ${subscription}`); + + if (!subscription) { + return true; + } + + const request = context.switchToHttp().getRequest(); + const user = request.requestor; + this.logger.log(`user request:: orgId: ${user.orgId}`); + + const userPermissions = user.userRoleOrgPermissions[0].role.permissions; + + const permsArray = []; + + userPermissions.every( + permissions => permsArray.push(permissions.name) + ); + + return this.matchRoles(permsArray, permissions); + } + + matchRoles(UserPermissions: string[], APIPermissions: string[]): boolean { + this.logger.log('called matches permission'); + + const checker = APIPermissions.some(function (val) { + return 0 <= UserPermissions.indexOf(val); + }); + + if (checker) { + return true; + } + return false; + } +} diff --git a/apps/api-gateway/src/authz/socket.gateway.ts b/apps/api-gateway/src/authz/socket.gateway.ts new file mode 100644 index 000000000..a8a9bc44b --- /dev/null +++ b/apps/api-gateway/src/authz/socket.gateway.ts @@ -0,0 +1,105 @@ +import { + OnGatewayConnection, + SubscribeMessage, + WebSocketGateway, + WebSocketServer + +} from '@nestjs/websockets'; + +import { AgentService } from '../agent/agent.service'; +import { ConnectionService } from '../connection/connection.service'; +import { Logger } from '@nestjs/common'; +import { VerificationService } from '../verification/verification.service'; +import { ISocketInterface } from '../interfaces/ISocket.interface'; + +@WebSocketGateway() +export class SocketGateway implements OnGatewayConnection { + @WebSocketServer() server; + + constructor( + private readonly verificationService: VerificationService, + private readonly connectionService: ConnectionService, + private readonly agentService: AgentService + ) { } + private readonly logger = new Logger('SocketGateway'); + + handleConnection(): void { + this.logger.debug(`Socket connected.`); + } + + /** + * @description:Method used to disconnect the socket. + */ + handleDisconnect(): void { + this.logger.debug(`Socket disconnected.`); + } + + // @SubscribeMessage('message') + // async handleMessage(client: Socket): Promise { + // const generatedProofRequest: ResponseService = await this.verificationService.generateProofRequestPasswordLess(); + // this.server.to(client.id).emit('message', generatedProofRequest); + // } + + @SubscribeMessage('passwordLess') + async handlePasswordLessResponse(payload: ISocketInterface): Promise { + this.server.to(payload.clientSocketId).emit('passwordLess', payload.token); + } + + @SubscribeMessage('agent-spinup-process-initiated') + async handlAgentSpinUpProccessStartedResponse( + client: string, + payload: ISocketInterface + ): Promise { + this.server.to(payload.clientId).emit('agent-spinup-process-initiated'); + } + + @SubscribeMessage('agent-spinup-process-completed') + async handlAgentSpinUpProccessSucessResponse( + client: string, + payload: ISocketInterface + ): Promise { + this.server.to(payload.clientId).emit('agent-spinup-process-completed'); + } + + @SubscribeMessage('did-publish-process-initiated') + async handlDidPublicProcessStarted( + client: string, + payload: ISocketInterface + ): Promise { + this.server.to(payload.clientId).emit('did-publish-process-initiated'); + } + + @SubscribeMessage('did-publish-process-completed') + async handlDidPublicProcessSuccess( + client: string, + payload: ISocketInterface + ): Promise { + this.server.to(payload.clientId).emit('did-publish-process-completed'); + } + + @SubscribeMessage('invitation-url-creation-started') + async handleInvitationUrlCreationStartResponse( + client: string, + payload: ISocketInterface + ): Promise { + this.logger.log(`invitation-url-creation-started ${payload.clientId}`); + this.server.to(payload.clientId).emit('invitation-url-creation-started'); + } + + @SubscribeMessage('invitation-url-creation-success') + async handleInvitationUrlCreationSuccessResponse( + client: string, + payload: ISocketInterface + ): Promise { + this.logger.log(`invitation-url-creation-success ${payload.clientId}`); + this.server.to(payload.clientId).emit('invitation-url-creation-success'); + } + + @SubscribeMessage('error-in-wallet-creation-process') + async handleErrorResponse(payload: ISocketInterface): Promise { + this.logger.log(`error-in-wallet-creation-process ${payload.clientId}`); + this.server + .to(payload.clientId) + .emit('error-in-wallet-creation-process', payload.error); + } +} diff --git a/apps/api-gateway/src/config/multer.config.ts b/apps/api-gateway/src/config/multer.config.ts new file mode 100644 index 000000000..ba46cf06b --- /dev/null +++ b/apps/api-gateway/src/config/multer.config.ts @@ -0,0 +1,25 @@ +import { HttpException, HttpStatus } from '@nestjs/common'; +import { diskStorage } from 'multer'; +import * as fs from 'fs'; + + +// Multer upload options +export const multerCSVOptions = { + storage: diskStorage({ + destination: (req, file, cb) => { + const { id } = req.body; + const path = `./uploadedFiles/import`; + fs.mkdirSync(path, { recursive: true }); + return cb(null, path); + }, + filename: (req, file, cb) => { + if ( + 'text/csv' === file.mimetype + ) { + cb(null, `${file.originalname}`); + } else { + cb(new HttpException(`File format should be CSV`, HttpStatus.BAD_REQUEST), ''); + } + } + }) +}; diff --git a/apps/api-gateway/src/connection/connection.controller.ts b/apps/api-gateway/src/connection/connection.controller.ts new file mode 100644 index 000000000..51619e0e0 --- /dev/null +++ b/apps/api-gateway/src/connection/connection.controller.ts @@ -0,0 +1,199 @@ +import IResponseType from '@credebl/common/interfaces/response.interface'; +import { ResponseMessages } from '@credebl/common/response-messages'; +import { Controller, Logger, Post, Body, UseGuards, HttpStatus, Res, Get, Param, Query } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; +import { ApiBearerAuth, ApiExcludeEndpoint, ApiForbiddenResponse, ApiOperation, ApiQuery, ApiResponse, ApiTags, ApiUnauthorizedResponse } from '@nestjs/swagger'; +import { User } from '../authz/decorators/user.decorator'; +import { AuthTokenResponse } from '../authz/dtos/auth-token-res.dto'; +import { ForbiddenErrorDto } from '../dtos/forbidden-error.dto'; +import { UnauthorizedErrorDto } from '../dtos/unauthorized-error.dto'; +import { ConnectionService } from './connection.service'; +import { ConnectionDto, CreateConnectionDto } from './dtos/connection.dto'; +import { IUserRequestInterface } from './interfaces'; +import { Response } from 'express'; +import { Connections } from './enums/connections.enum'; +import { IUserRequest } from '@credebl/user-request/user-request.interface'; + +@Controller() +@ApiTags('connections') +@ApiUnauthorizedResponse({ status: 401, description: 'Unauthorized', type: UnauthorizedErrorDto }) +@ApiForbiddenResponse({ status: 403, description: 'Forbidden', type: ForbiddenErrorDto }) +export class ConnectionController { + + private readonly logger = new Logger('Connection'); + constructor(private readonly connectionService: ConnectionService + ) { + /** + * Create out-of-band connection legacy invitation + * @param connectionDto + * @param res + * @returns Created out-of-band connection invitation url + */ + } + @Post('/connections') + @ApiOperation({ summary: 'Create outbound out-of-band connection (Legacy Invitation)', description: 'Create outbound out-of-band connection (Legacy Invitation)' }) + @UseGuards(AuthGuard('jwt')) + @ApiBearerAuth() + @ApiResponse({ status: 200, description: 'Success', type: AuthTokenResponse }) + async createLegacyConnectionInvitation(@Body() connectionDto: CreateConnectionDto, @User() reqUser: IUserRequestInterface, @Res() res: Response): Promise { + const connectionData = await this.connectionService.createLegacyConnectionInvitation(connectionDto, reqUser); + const finalResponse: IResponseType = { + statusCode: HttpStatus.OK, + message: ResponseMessages.connection.success.create, + data: connectionData.response + }; + return res.status(HttpStatus.OK).json(finalResponse); + + } + + + /** + * Description: Get all connections + * @param user + * @param threadId + * @param connectionId + * @param state + * @param orgId + * + */ + @Get('/connections') + @UseGuards(AuthGuard('jwt')) + @ApiBearerAuth() + @ApiOperation({ + summary: `Fetch all connections details`, + description: `Fetch all connections details` + }) + @ApiResponse({ status: 201, description: 'Success', type: AuthTokenResponse }) + @ApiQuery( + { name: 'outOfBandId', required: false } + ) + @ApiQuery( + { name: 'alias', required: false } + ) + @ApiQuery( + { name: 'state', enum: Connections, required: false } + ) + @ApiQuery( + { name: 'myDid', required: false } + ) + @ApiQuery( + { name: 'theirDid', required: false } + ) + @ApiQuery( + { name: 'theirLabel', required: false } + ) + @ApiQuery( + { name: 'orgId', required: true } + ) + + async getConnections( + @User() user: IUserRequest, + @Query('outOfBandId') outOfBandId: string, + @Query('alias') alias: string, + @Query('state') state: string, + @Query('myDid') myDid: string, + @Query('theirDid') theirDid: string, + @Query('theirLabel') theirLabel: string, + @Query('orgId') orgId: number, + @Res() res: Response + ): Promise { + + // eslint-disable-next-line no-param-reassign + state = state || undefined; + const connectionDetails = await this.connectionService.getConnections(user, outOfBandId, alias, state, myDid, theirDid, theirLabel, orgId); + + const finalResponse: IResponseType = { + statusCode: HttpStatus.CREATED, + message: ResponseMessages.connection.success.fetch, + data: connectionDetails.response + }; + return res.status(HttpStatus.CREATED).json(finalResponse); + } + + + /** + * Catch connection webhook responses. + * @Body connectionDto + * @param id + * @param res + */ + + @Post('wh/:id/connections/') + @ApiExcludeEndpoint() + @ApiOperation({ + summary: 'Catch connection webhook responses', + description: 'Callback URL for connection' + }) + @ApiResponse({ status: 200, description: 'Success', type: AuthTokenResponse }) + async getConnectionWebhook( + @Body() connectionDto: ConnectionDto, + @Param('id') id: number, + @Res() res: Response + ): Promise { + const connectionData = await this.connectionService.getConnectionWebhook(connectionDto, id); + const finalResponse: IResponseType = { + statusCode: HttpStatus.OK, + message: ResponseMessages.connection.success.create, + data: connectionData + }; + return res.status(HttpStatus.OK).json(finalResponse); + } + + /** + * Shortening url based on reference Id. + * @param referenceId The referenceId is set as a request parameter. + * @param res The current url is set as a header in the response parameter. + */ + @Get('connections/url/:referenceId') + @ApiOperation({ + summary: 'Shortening url based on reference Id', + description: 'Shortening url based on reference Id' + }) + async getPresentproofRequestUrl( + @Param('referenceId') referenceId: string, + @Res() res: Response + ): Promise { + const originalUrlData = await this.connectionService.getUrl(referenceId); + + const finalResponse: IResponseType = { + statusCode: HttpStatus.OK, + message: ResponseMessages.connection.success.create, + data: originalUrlData.response + }; + return res.status(HttpStatus.OK).json(finalResponse.data); + } + + /** +* Description: Get all connections by connectionId +* @param user +* @param connectionId +* @param orgId +* +*/ + @Get('connections/:connectionId') + @UseGuards(AuthGuard('jwt')) + @ApiBearerAuth() + @ApiOperation({ + summary: `Fetch all connections details by connectionId`, + description: `Fetch all connections details by connectionId` + }) + @ApiQuery( + { name: 'orgId', required: true } + ) + @ApiResponse({ status: 201, description: 'Success', type: AuthTokenResponse }) + async getConnectionsById( + @User() user: IUserRequest, + @Param('connectionId') connectionId: string, + @Query('orgId') orgId: number, + @Res() res: Response + ): Promise { + const connectionsDetails = await this.connectionService.getConnectionsById(user, connectionId, orgId); + + const finalResponse: IResponseType = { + statusCode: HttpStatus.CREATED, + message: ResponseMessages.connection.success.fetch, + data: connectionsDetails.response + }; + return res.status(HttpStatus.CREATED).json(finalResponse); + } +} diff --git a/apps/api-gateway/src/connection/connection.module.ts b/apps/api-gateway/src/connection/connection.module.ts new file mode 100644 index 000000000..4568fa599 --- /dev/null +++ b/apps/api-gateway/src/connection/connection.module.ts @@ -0,0 +1,24 @@ +import { ConnectionController } from './connection.controller'; +import { ConnectionService } from './connection.service'; +import { Module } from '@nestjs/common'; +import { ClientsModule, Transport } from '@nestjs/microservices'; + +@Module({ + imports: [ + + ClientsModule.register([ + { + name: 'NATS_CLIENT', + transport: Transport.NATS, + options: { + servers: [`${process.env.NATS_URL}`] + } + } + ]) + ], + controllers: [ConnectionController], + providers: [ConnectionService] +}) + +export class ConnectionModule { +} \ No newline at end of file diff --git a/apps/api-gateway/src/connection/connection.service.ts b/apps/api-gateway/src/connection/connection.service.ts new file mode 100644 index 000000000..1346b2708 --- /dev/null +++ b/apps/api-gateway/src/connection/connection.service.ts @@ -0,0 +1,61 @@ +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 } from './dtos/connection.dto'; +import { IUserRequestInterface } from './interfaces'; + + +@Injectable() +export class ConnectionService extends BaseService { + constructor( + @Inject('NATS_CLIENT') private readonly connectionServiceProxy: ClientProxy + ) { + super('ConnectionService'); + } + + createLegacyConnectionInvitation(connectionDto: CreateConnectionDto, user: IUserRequestInterface): Promise<{ + response: object; + }> { + try { + const connectionDetails = { orgId: connectionDto.orgId, alias: connectionDto.alias, label: connectionDto.label, imageUrl: connectionDto.imageUrl, multiUseInvitation: connectionDto.multiUseInvitation, autoAcceptConnection: connectionDto.autoAcceptConnection, user }; + return this.sendNats(this.connectionServiceProxy, 'create-connection', connectionDetails); + } catch (error) { + throw new RpcException(error.response); + + } + } + + getConnectionWebhook(connectionDto: ConnectionDto, id: number): Promise<{ + response: object; + }> { + const payload = { connectionId: connectionDto.id, state: connectionDto.state, orgDid: connectionDto.theirDid, theirLabel: connectionDto.theirLabel, autoAcceptConnection: connectionDto.autoAcceptConnection, outOfBandId: connectionDto.outOfBandId, createDateTime: connectionDto.createdAt, lastChangedDateTime: connectionDto.updatedAt, orgId: id }; + return this.sendNats(this.connectionServiceProxy, 'webhook-get-connection', payload); + } + + getUrl(referenceId: string): Promise<{ + response: object; + }> { + try { + const connectionDetails = { referenceId }; + return this.sendNats(this.connectionServiceProxy, 'get-connection-url', connectionDetails); + } catch (error) { + throw new RpcException(error.response); + + } + } + + getConnections(user: IUserRequest, outOfBandId: string, alias: string, state: string, myDid: string, theirDid: string, theirLabel: string, orgId: number): Promise<{ + response: object; + }> { + const payload = { user, outOfBandId, alias, state, myDid, theirDid, theirLabel, orgId }; + return this.sendNats(this.connectionServiceProxy, 'get-all-connections', payload); + } + + getConnectionsById(user: IUserRequest, connectionId: string, orgId: number): Promise<{ + response: object; + }> { + const payload = { user, connectionId, orgId }; + return this.sendNats(this.connectionServiceProxy, 'get-all-connections-by-connectionId', payload); + } +} \ No newline at end of file diff --git a/apps/api-gateway/src/connection/dtos/connection.dto.ts b/apps/api-gateway/src/connection/dtos/connection.dto.ts new file mode 100644 index 000000000..e8bb7c55a --- /dev/null +++ b/apps/api-gateway/src/connection/dtos/connection.dto.ts @@ -0,0 +1,100 @@ +import { IsBoolean, IsNotEmpty, IsNumber, IsOptional, IsString } from 'class-validator'; + +import { ApiProperty } from '@nestjs/swagger'; + +export class CreateConnectionDto { + @ApiProperty() + @IsOptional() + @IsString({ message: 'alias must be a string' }) @IsNotEmpty({ message: 'please provide valid alias' }) + alias: string; + + @ApiProperty() + @IsOptional() + @IsString({ message: 'label must be a string' }) @IsNotEmpty({ message: 'please provide valid label' }) + label: string; + + @ApiProperty() + @IsOptional() + @IsNotEmpty({ message: 'please provide valid imageUrl' }) + imageUrl: string; + + @ApiProperty() + @IsBoolean() + @IsOptional() + @IsNotEmpty({ message: 'please provide multiUseInvitation' }) + multiUseInvitation: boolean; + + @ApiProperty() + @IsBoolean() + @IsOptional() + @IsNotEmpty({ message: 'please provide valid autoAcceptConnection' }) + autoAcceptConnection: boolean; + + @ApiProperty() + @IsNumber() + @IsNotEmpty({ message: 'please provide orgId' }) + orgId: number; +} + + +export class ConnectionDto { + @ApiProperty() + @IsOptional() + _tags?: object; + + @ApiProperty() + @IsOptional() + metadata: object; + + @ApiProperty() + @IsOptional() + connectionTypes: object[]; + + @ApiProperty() + @IsOptional() + id: string; + + @ApiProperty() + @IsOptional() + createdAt: string; + + @ApiProperty() + @IsOptional() + did: string; + + @ApiProperty() + @IsOptional() + theirDid: string; + + @ApiProperty() + @IsOptional() + theirLabel: string; + + @ApiProperty() + @IsOptional() + state: string; + + @ApiProperty() + @IsOptional() + role: string; + + @ApiProperty() + @IsOptional() + autoAcceptConnection: boolean; + + @ApiProperty() + @IsOptional() + threadId: string; + + @ApiProperty() + @IsOptional() + protocol: string; + + @ApiProperty() + @IsOptional() + outOfBandId: string; + + @ApiProperty() + @IsOptional() + updatedAt: 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 new file mode 100644 index 000000000..3ea871b2a --- /dev/null +++ b/apps/api-gateway/src/connection/enums/connections.enum.ts @@ -0,0 +1,12 @@ +export enum Connections { + start = 'start', + invitationSent = 'invitation-sent', + invitationReceived = 'invitation-received', + requestSent = 'request-sent', + declined = 'decliend', + requestReceived = 'request-received', + responseSent = 'response-sent', + responseReceived = 'response-received', + complete = 'complete', + abandoned = 'abandoned' +} \ No newline at end of file diff --git a/apps/api-gateway/src/connection/interfaces/index.ts b/apps/api-gateway/src/connection/interfaces/index.ts new file mode 100644 index 000000000..39d031ef3 --- /dev/null +++ b/apps/api-gateway/src/connection/interfaces/index.ts @@ -0,0 +1,55 @@ +import { UserRoleOrgPermsDto } from '../../dtos/user-role-org-perms.dto'; + +export interface IUserRequestInterface { + userId: number; + email: string; + orgId: number; + agentEndPoint?: string; + apiKey?: string; + tenantId?: number; + tenantName?: string; + tenantOrgId?: number; + userRoleOrgPermissions?: UserRoleOrgPermsDto[]; + orgName?: string; + selectedOrg: ISelectedOrgInterface; +} + +export interface ISelectedOrgInterface { + id: number; + userId: number; + orgRoleId: number; + orgId: number; + orgRole: object; + organisation: object; +} + +export interface IOrganizationInterface { + name: string; + description: string; + org_agents: IOrgAgentInterface[] + +} + +export interface IOrgAgentInterface { + orgDid: string; + verkey: string; + agentEndPoint: string; + agentOptions: string; + walletName: string; + agentsTypeId: string; + orgId: string; +} + + +export class IConnectionInterface { + tag: object; + createdAt: string; + updatedAt: string; + connectionId: string; + state: string; + orgDid: string; + theirLabel: string; + autoAcceptConnection: boolean; + outOfBandId: string; + orgId: number; +} diff --git a/apps/api-gateway/src/credential-definition/credential-definition.controller.spec.ts b/apps/api-gateway/src/credential-definition/credential-definition.controller.spec.ts new file mode 100644 index 000000000..2ad6e017a --- /dev/null +++ b/apps/api-gateway/src/credential-definition/credential-definition.controller.spec.ts @@ -0,0 +1,204 @@ +import { Test, TestingModule } from '@nestjs/testing'; + +import { Any } from 'typeorm'; +import { CredentialDefinitionController } from './credential-definition.controller'; +import { CredentialDefinitionService } from './credential-definition.service'; + +describe('CredentialDefinitionController Test Cases', () => { + let controller: CredentialDefinitionController; + const mockCredentialDefinitionService = { + createCredentialDefinition: jest.fn(() => ({})), + getAllCredDefsByOrgId: jest.fn(() => ({})), + getCredDefsByCredId: jest.fn(() => ({})), + getAllCredentialDefinitionForHolder: jest.fn(() => ({})) + }; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [CredentialDefinitionController], + providers: [CredentialDefinitionService] + }) + .overrideProvider(CredentialDefinitionService) + .useValue(mockCredentialDefinitionService) + .compile(); + controller = module.get( + CredentialDefinitionController + ); + }); + describe('createCredential', () => { + const user: any = {}; + user.orgId = 1234; + const createCredentialDefinition: any = { + schema_id: 'Test', + tag: 'Test', + support_revocation: true, + support_auto_issue: true, + revocation_registry_size: 0 + }; + it('should return an expected credentialdefinition', async () => { + const result = await controller.createCredential( + user, + createCredentialDefinition + ); + expect(result).toEqual({}); + }); + + it('should check returned credentialdefinition is not to be null', async () => { + const result = await controller.createCredential( + user, + createCredentialDefinition + ); + expect(result).not.toBeNull(); + }); + it('should hit error if support_auto_issue is number', async () => { + createCredentialDefinition.support_auto_issue = 1234; + const result = await (() => { + controller.createCredential(user, createCredentialDefinition); + }); + expect(result).toThrowError('Support auto issue should be boolean.'); + }); + it('should hit error if support_auto_issue is empty', async () => { + createCredentialDefinition.support_auto_issue = ''; + const result = await (() => { + controller.createCredential(user, createCredentialDefinition); + }); + expect(result).toThrowError('Please provide support auto issue data.'); + }); + it('should hit error if support_revocation is empty', async () => { + createCredentialDefinition.support_revocation = ''; + const result = await (() => { + controller.createCredential(user, createCredentialDefinition); + }); + expect(result).toThrowError('Please provide support revocation data.'); + }); + it('should hit error if support_revocation is number', async () => { + createCredentialDefinition.support_revocation = 1234; + const result = await (() => { + controller.createCredential(user, createCredentialDefinition); + }); + expect(result).toThrowError('Support revocation should be boolean.'); + }); + it('should hit error if tag is empty', async () => { + createCredentialDefinition.tag = ''; + const result = await (() => { + controller.createCredential(user, createCredentialDefinition); + }); + expect(result).toThrowError('Please provide a valid tag.'); + }); + it('should hit error if tag is number', async () => { + createCredentialDefinition.tag = 1234; + const result = await (() => { + controller.createCredential(user, createCredentialDefinition); + }); + expect(result).toThrowError('Tag should be a string.'); + }); + it('should hit error if schema_id is empty', async () => { + createCredentialDefinition.schema_id = ''; + const result = await (() => { + controller.createCredential(user, createCredentialDefinition); + }); + expect(result).toThrowError('Please provide a schema id.'); + }); + it('should hit error if schema_id is number', async () => { + createCredentialDefinition.schema_id = 1234; + const result = await (() => { + controller.createCredential(user, createCredentialDefinition); + }); + expect(result).toThrowError('Schema id should be a string.'); + }); + //createCredential test case + + }); + describe('getAllCredDefsByOrgId', () => { + const page: any = 'hello'; + const search_text: any = 'Test'; + const items_per_page: any = 1234; + const orgId: any = 1234; + const credDefSortBy: any = 1234; + const sortValue: any = 1234; + const supportRevocation: any = 'Test'; + const user: any = 1234; + it('should return an expected credentialdefinition', async () => { + const result = await controller.getAllCredDefsByOrgId( + page, + search_text, + items_per_page, + orgId, + credDefSortBy, + sortValue, + supportRevocation, + user + ); + expect(result).toEqual({}); + }); + it('should return an expected credentialdefinition', async () => { + const result = await controller.getAllCredDefsByOrgId( + page, + search_text, + items_per_page, + orgId, + credDefSortBy, + sortValue, + supportRevocation, + user + ); + expect(result).not.toBeNull(); + }); + }); + // describe("getAllCredDefsByOrgId", () => { + // let page: any = "hello"; + + // it("should return an expected credentialdefinition", async () => { + // const result = await controller.getCredDefsByCredId( + // page, + // search_text, + + // ); + // expect(result).toEqual({}); + // }); + // it("should return an expected credentialdefinition", async () => { + // const result = await controller.getCredDefsByCredId( + // page, + // search_text, + + // ); + // expect(result).not.toBeNull(); + // }); + // }); + describe('getAllCredentialDefinitionForHolder', () => { + const page: any = 'hello'; + const search_text: any = 'Test'; + const items_per_page: any = 1234; + const orgId: any = 1234; + const credDefSortBy: any = 1234; + const sortValue: any = 1234; + const supportRevocation: any = 'Test'; + const user: any = 1234; + it('should return an expected credentialdefinition', async () => { + const result = await controller.getAllCredentialDefinitionForHolder( + page, + search_text, + items_per_page, + orgId, + credDefSortBy, + sortValue, + supportRevocation, + user + ); + expect(result).toEqual({}); + }); + it('should return an expected credentialdefinition', async () => { + const result = await controller.getAllCredDefsByOrgId( + page, + search_text, + items_per_page, + orgId, + credDefSortBy, + sortValue, + supportRevocation, + user + ); + expect(result).not.toBeNull(); + }); + }); +}); diff --git a/apps/api-gateway/src/credential-definition/credential-definition.controller.ts b/apps/api-gateway/src/credential-definition/credential-definition.controller.ts new file mode 100644 index 000000000..be8e25a0f --- /dev/null +++ b/apps/api-gateway/src/credential-definition/credential-definition.controller.ts @@ -0,0 +1,102 @@ +import { Controller, Logger, Post, Body, UseGuards, Get, Query, HttpStatus, Res } from '@nestjs/common'; +import { CredentialDefinitionService } from './credential-definition.service'; +import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiUnauthorizedResponse, ApiForbiddenResponse, ApiQuery } from '@nestjs/swagger'; +import { ApiResponseDto } from 'apps/api-gateway/src/dtos/apiResponse.dto'; +import { UnauthorizedErrorDto } from 'apps/api-gateway/src/dtos/unauthorized-error.dto'; +import { ForbiddenErrorDto } from 'apps/api-gateway/src/dtos/forbidden-error.dto'; +import { User } from '../authz/decorators/user.decorator'; +import { AuthGuard } from '@nestjs/passport'; +import IResponseType from '@credebl/common/interfaces/response.interface'; +import { ResponseMessages } from '@credebl/common/response-messages'; +import { Response } from 'express'; +import { GetAllCredDefsDto } from './dto/get-all-cred-defs.dto'; +import { OrgRolesGuard } from '../authz/guards/org-roles.guard'; +import { IUserRequestInterface } from './interfaces'; +import { CreateCredentialDefinitionDto } from './dto/create-cred-defs.dto'; +import { OrgRoles } from 'libs/org-roles/enums'; +import { Roles } from '../authz/decorators/roles.decorator'; + + +@ApiBearerAuth() +@UseGuards(AuthGuard('jwt'), OrgRolesGuard) +@Roles(OrgRoles.OWNER, OrgRoles.SUPER_ADMIN, OrgRoles.ADMIN, OrgRoles.ISSUER) +@ApiTags('credential-definitions') + +@ApiUnauthorizedResponse({ status: 401, description: 'Unauthorized', type: UnauthorizedErrorDto }) +@ApiForbiddenResponse({ status: 403, description: 'Forbidden', type: ForbiddenErrorDto }) +@Controller('credential-definitions') +export class CredentialDefinitionController { + + constructor(private readonly credentialDefinitionService: CredentialDefinitionService) { } + private readonly logger = new Logger('CredentialDefinitionController'); + + @Post('/') + @ApiOperation({ + summary: 'Sends a credential definition to the ledger', + description: 'Create and sends a credential definition to the ledger.' + }) + @ApiResponse({ status: 201, description: 'Success', type: ApiResponseDto }) + async createCredentialDefinition( + @User() user: IUserRequestInterface, + @Body() credDef: CreateCredentialDefinitionDto, + @Res() res: Response + ): Promise { + const credentialsDefinitionDetails = await this.credentialDefinitionService.createCredentialDefinition(credDef, user); + const credDefResponse: IResponseType = { + statusCode: HttpStatus.CREATED, + message: ResponseMessages.credentialDefinition.success.create, + data: credentialsDefinitionDetails.response + }; + return res.status(HttpStatus.OK).json(credDefResponse); + } + @Get('/id') + @ApiOperation({ + summary: 'Get an existing credential definition by Id', + description: 'Get an existing credential definition by Id' + }) + @ApiQuery( + { name: 'credentialDefinitionId', required: true } + ) + @ApiQuery( + { name: 'orgId', required: true } + ) + @ApiResponse({ status: 201, description: 'Success', type: ApiResponseDto }) + async getCredentialDefinitionById( + @Query('credentialDefinitionId') credentialDefinitionId: string, + @Query('orgId') orgId: number, + @Res() res: Response + ): Promise { + const credentialsDefinitionDetails = await this.credentialDefinitionService.getCredentialDefinitionById(credentialDefinitionId, orgId); + const credDefResponse: IResponseType = { + statusCode: HttpStatus.OK, + message: ResponseMessages.credentialDefinition.success.fetch, + data: credentialsDefinitionDetails.response + }; + return res.status(HttpStatus.OK).json(credDefResponse); + } + + @Get('/') + @ApiOperation({ + summary: 'Fetch all credential definitions of provided organization id with pagination', + description: 'Fetch all credential definitions from metadata saved in database of provided organization id.' + }) + async getAllCredDefs( + @Query() getAllCredDefs: GetAllCredDefsDto, + @User() user: IUserRequestInterface, + @Res() res: Response + ): Promise { + const { pageSize, pageNumber, sortByValue, sorting, orgId, searchByText, revocable } = getAllCredDefs; + const credDefSearchCriteria = { pageSize, pageNumber, searchByText, sorting, sortByValue, revocable }; + const credentialsDefinitionDetails = await this.credentialDefinitionService.getAllCredDefs( + credDefSearchCriteria, + user, + orgId + ); + const credDefResponse: IResponseType = { + statusCode: HttpStatus.OK, + message: ResponseMessages.credentialDefinition.success.fetch, + data: credentialsDefinitionDetails.response + }; + return res.status(HttpStatus.OK).json(credDefResponse); + } +} \ No newline at end of file diff --git a/apps/api-gateway/src/credential-definition/credential-definition.module.ts b/apps/api-gateway/src/credential-definition/credential-definition.module.ts new file mode 100644 index 000000000..c7ffed996 --- /dev/null +++ b/apps/api-gateway/src/credential-definition/credential-definition.module.ts @@ -0,0 +1,27 @@ +import { ClientsModule, Transport } from '@nestjs/microservices'; +import { Logger, Module } from '@nestjs/common'; + +import { CredentialDefinitionController } from './credential-definition.controller'; +import { CredentialDefinitionService } from './credential-definition.service'; + +@Module({ + imports:[ + ClientsModule.register([ + { + name: 'NATS_CLIENT', + transport: Transport.NATS, + options: { + servers: [`${process.env.NATS_URL}`] + } + } + ]) + ], + controllers: [CredentialDefinitionController], + providers: [CredentialDefinitionService] +}) +export class CredentialDefinitionModule { + constructor() { + Logger.log('API Gateway - CredDef loaded...'); + + } +} diff --git a/apps/api-gateway/src/credential-definition/credential-definition.service.spec.ts b/apps/api-gateway/src/credential-definition/credential-definition.service.spec.ts new file mode 100644 index 000000000..8d6322afc --- /dev/null +++ b/apps/api-gateway/src/credential-definition/credential-definition.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { CredentialDefinitionService } from './credential-definition.service'; + +describe('CredentialDefinitionService', () => { + let service: CredentialDefinitionService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [CredentialDefinitionService] + }).compile(); + + service = module.get(CredentialDefinitionService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/apps/api-gateway/src/credential-definition/credential-definition.service.ts b/apps/api-gateway/src/credential-definition/credential-definition.service.ts new file mode 100644 index 000000000..36f1dbf91 --- /dev/null +++ b/apps/api-gateway/src/credential-definition/credential-definition.service.ts @@ -0,0 +1,46 @@ +import { Injectable, Inject } from '@nestjs/common'; +import { ClientProxy, RpcException } from '@nestjs/microservices'; +import { CreateCredentialDefinitionDto } from './dto/create-cred-defs.dto'; +import { BaseService } from '../../../../libs/service/base.service'; +import { IUserRequestInterface } from '../interfaces/IUserRequestInterface'; +import { GetAllCredDefsDto } from '../dtos/get-cred-defs.dto'; + +@Injectable() +export class CredentialDefinitionService extends BaseService { + + constructor( + @Inject('NATS_CLIENT') private readonly credDefServiceProxy: ClientProxy + ) { + super('CredentialDefinitionService'); + } + + createCredentialDefinition(credDef: CreateCredentialDefinitionDto, user: IUserRequestInterface): Promise<{ response: object }> { + try { + const payload = { credDef, user }; + return this.sendNats(this.credDefServiceProxy, 'create-credential-definition', payload); + } catch (error) { + throw new RpcException(error.response); + + } + } + + getCredentialDefinitionById(credentialDefinitionId: string, orgId: number): Promise<{ response: object }> { + try { + const payload = { credentialDefinitionId, orgId }; + return this.sendNats(this.credDefServiceProxy, 'get-credential-definition-by-id', payload); + } catch (error) { + throw new RpcException(error.response); + + } + } + + getAllCredDefs(credDefSearchCriteria: GetAllCredDefsDto, user: IUserRequestInterface, orgId: number): Promise<{ response: object }> { + try { + const payload = { credDefSearchCriteria, user, orgId }; + return this.sendNats(this.credDefServiceProxy, 'get-all-credential-definitions', payload); + } catch (error) { + throw new RpcException(error.response); + + } + } +} diff --git a/apps/api-gateway/src/credential-definition/dto/create-cred-defs.dto.ts b/apps/api-gateway/src/credential-definition/dto/create-cred-defs.dto.ts new file mode 100644 index 000000000..0754ebfd8 --- /dev/null +++ b/apps/api-gateway/src/credential-definition/dto/create-cred-defs.dto.ts @@ -0,0 +1,32 @@ +import { IsBoolean, IsDefined, IsNotEmpty, IsNumber, IsOptional, IsString } from 'class-validator'; + +import { ApiProperty } from '@nestjs/swagger'; + +export class CreateCredentialDefinitionDto { + + @ApiProperty({ 'example': 'default' }) + @IsNotEmpty({ message: 'Please provide a tag' }) + @IsString({ message: 'Tag id should be string' }) + tag: string; + + @ApiProperty({ 'example': 'WgWxqztrNooG92RXvxSTWv:2:schema_name:1.0' }) + @IsNotEmpty({ message: 'Please provide a schema id' }) + @IsString({ message: 'Schema id should be string' }) + schemaLedgerId: string; + + @ApiProperty() + @IsNumber() + @IsNotEmpty({ message: 'Please provide orgId' }) + orgId: number; + + @ApiProperty({ required: false }) + @IsOptional() + @IsString({ message: 'orgDid must be a string' }) + orgDid: string; + + @ApiProperty({ default: false }) + @IsDefined({ message: 'Revocable is required.' }) + @IsBoolean({ message: 'Revocable must be a boolean value.' }) + @IsNotEmpty({ message: 'Please provide whether the revocable must be true or false' }) + revocable: boolean; +} diff --git a/apps/api-gateway/src/credential-definition/dto/get-all-cred-defs.dto.ts b/apps/api-gateway/src/credential-definition/dto/get-all-cred-defs.dto.ts new file mode 100644 index 000000000..76172f182 --- /dev/null +++ b/apps/api-gateway/src/credential-definition/dto/get-all-cred-defs.dto.ts @@ -0,0 +1,47 @@ +/* eslint-disable @typescript-eslint/no-inferrable-types */ +/* eslint-disable camelcase */ +import { ApiProperty } from '@nestjs/swagger'; +import { SortValue } from '../../enum'; +import { Transform, Type } from 'class-transformer'; +import { trim } from '@credebl/common/cast.helper'; +import { IsNotEmpty, IsNumber, IsOptional } from 'class-validator'; + +export class GetAllCredDefsDto { + @ApiProperty({ required: false }) + @IsOptional() + @Type(() => Number) + @Transform(({ value }) => trim(value)) + pageNumber: number = 1; + + @ApiProperty({ required: false }) + @IsOptional() + @Type(() => String) + @Transform(({ value }) => trim(value)) + searchByText: string = ''; + + @ApiProperty({ required: false }) + @IsOptional() + @Type(() => Number) + @Transform(({ value }) => trim(value)) + pageSize: number = 10; + + @ApiProperty({ required: false }) + @IsOptional() + @Transform(({ value }) => trim(value)) + sorting: string = 'id'; + + @ApiProperty({ required: false }) + @IsOptional() + sortByValue: string = SortValue.DESC; + + @ApiProperty({ required: false }) + @IsOptional() + revocable: boolean = true; + + @ApiProperty({ required: true }) + @Type(() => Number) + @IsNumber() + @IsNotEmpty() + orgId: number; +} + diff --git a/apps/api-gateway/src/credential-definition/interfaces/index.ts b/apps/api-gateway/src/credential-definition/interfaces/index.ts new file mode 100644 index 000000000..650f24143 --- /dev/null +++ b/apps/api-gateway/src/credential-definition/interfaces/index.ts @@ -0,0 +1,41 @@ +import { UserRoleOrgPermsDto } from '../../dtos/user-role-org-perms.dto'; + +export interface IUserRequestInterface { + userId: number; + email: string; + orgId: number; + agentEndPoint?: string; + apiKey?: string; + tenantId?: number; + tenantName?: string; + tenantOrgId?: number; + userRoleOrgPermissions?: UserRoleOrgPermsDto[]; + orgName?: string; + selectedOrg: ISelectedOrgInterface; +} + +export interface ISelectedOrgInterface { + id: number; + userId: number; + orgRoleId: number; + orgId: number; + orgRole: object; + organisation: object; +} + +export interface IOrganizationInterface { + name: string; + description: string; + org_agents: IOrgAgentInterface[] + +} + +export interface IOrgAgentInterface { + orgDid: string; + verkey: string; + agentEndPoint: string; + agentOptions: string; + walletName: string; + agentsTypeId: string; + orgId: string; +} \ No newline at end of file diff --git a/apps/api-gateway/src/dtos/PresentProof.dto.ts b/apps/api-gateway/src/dtos/PresentProof.dto.ts new file mode 100644 index 000000000..20fd37643 --- /dev/null +++ b/apps/api-gateway/src/dtos/PresentProof.dto.ts @@ -0,0 +1,3 @@ +export class PresentProofDto { + credDef?: string; +} \ No newline at end of file diff --git a/apps/api-gateway/src/dtos/UpdateNonAdminUser.dto.ts b/apps/api-gateway/src/dtos/UpdateNonAdminUser.dto.ts new file mode 100644 index 000000000..3727fcb48 --- /dev/null +++ b/apps/api-gateway/src/dtos/UpdateNonAdminUser.dto.ts @@ -0,0 +1,15 @@ +import { IsArray, IsBoolean, IsInt, IsNotEmpty, IsString } from 'class-validator'; + +import { ApiProperty } from '@nestjs/swagger'; + +export class UpdateNonAdminUserDto { + @ApiProperty() + @IsNotEmpty({ message: 'Please provide valid organization id.' }) + @IsInt({ message: 'Organization id should be number.' }) + id: number; + + @ApiProperty() + @IsNotEmpty({ message: 'Please provide valid status.' }) + @IsBoolean({ message: 'Status should be boolean.' }) + isActive: boolean; +} diff --git a/apps/api-gateway/src/dtos/admin-onboard-user.dto.ts b/apps/api-gateway/src/dtos/admin-onboard-user.dto.ts new file mode 100644 index 000000000..c15e3f6bd --- /dev/null +++ b/apps/api-gateway/src/dtos/admin-onboard-user.dto.ts @@ -0,0 +1,67 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsString, IsInt } from 'class-validator'; +export class AdminOnBoardUserDto { + @ApiProperty() + // @IsNotEmpty({message:'Please provide valid orgName'}) + @IsString({message:'OrgName should be string'}) + orgName: string; + + @ApiProperty() + // @IsNotEmpty({message:'Please provide valid description'}) + @IsString({message:'Description should be string'}) + description: string; + + @ApiProperty() + // @IsNotEmpty({message:'Please provide valid displayName'}) + @IsString({message:'DisplayName should be string'}) + displayName: string; + + @ApiProperty() + // @IsNotEmpty({message:'Please provide valid logoUrl'}) + @IsString({message:'LogoUrl should be string'}) + logoUrl: string; + + @ApiProperty() + // @IsNotEmpty({message:'Please provide valid address'}) + @IsString({message:'Address should be string'}) + address: string; + + @ApiProperty() + // @IsNotEmpty({message:'Please provide valid adminEmail'}) + @IsString({message:'AdminEmail should be string'}) + adminEmail: string; + + @ApiProperty() + // @IsNotEmpty({message:'Please provide valid adminContact'}) + @IsString({message:'AdminContact should be string'}) + adminContact: string; + + @ApiProperty() + // @IsNotEmpty({message:'Please provide valid noOfUsers'}) + @IsInt({message:'NoOfUsers should be number'}) + noOfUsers: number; + + @ApiProperty() + @IsInt({message:'NoOfSchemas should be number'}) + noOfSchemas: number; + + @ApiProperty() + @IsInt({message:'NoOfCredentials should be number'}) + noOfCredentials: number; + + @ApiProperty() + @IsString({message:'AdminPassword should be string'}) + adminPassword: string; + + @ApiProperty() + @IsString({message:'AdminUsername should be string'}) + adminUsername: string; + + @ApiProperty() + @IsInt({message:'OrgCategory should be number'}) + orgCategory: number; + + @IsString({message:'ByAdmin should be string'}) + byAdmin?: string; + +} diff --git a/apps/api-gateway/src/dtos/admin-profile-update.dto.ts b/apps/api-gateway/src/dtos/admin-profile-update.dto.ts new file mode 100644 index 000000000..4f9533747 --- /dev/null +++ b/apps/api-gateway/src/dtos/admin-profile-update.dto.ts @@ -0,0 +1,49 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsString, IsNotEmpty } from 'class-validator'; + +export class AdminProfileDto { + @ApiProperty() + @IsNotEmpty({message:'Please provide valid displayName'}) + @IsString({message:'DisplayName should be string'}) + displayName?: string; + + @ApiProperty() + @IsNotEmpty({message:'Please provide valid description'}) + @IsString({message:'Description should be string'}) + description?: string; + + @ApiProperty() + @IsNotEmpty({message:'Please provide valid adminContact'}) + @IsString({message:'AdminContact should be string'}) + adminContact?: string; + + @ApiProperty() + @IsNotEmpty({message:'Please provide valid address'}) + @IsString({message:'Address should be string'}) + address?: string; + + @ApiProperty() + @IsNotEmpty({message:'Please provide valid logoUrl'}) + @IsString({message:'LogoUrl should be string'}) + logoUrl?: string; + + @ApiProperty() + @IsNotEmpty({message:'Please provide valid website'}) + @IsString({message:'Website should be string'}) + website?: string; + + @ApiProperty() + @IsNotEmpty({message:'Please provide valid solutionTitle'}) + @IsString({message:'SolutionTitle should be string'}) + solutionTitle?: string; + + @ApiProperty() + @IsNotEmpty({message:'Please provide valid solutionDesc'}) + @IsString({message:'SolutionDesc should be string'}) + solutionDesc?: string; + + @ApiProperty() + @IsNotEmpty({message:'Please provide valid tags'}) + @IsString({message:'Tags should be string'}) + tags?: string; +} \ No newline at end of file diff --git a/apps/api-gateway/src/dtos/apiResponse.dto copy.ts b/apps/api-gateway/src/dtos/apiResponse.dto copy.ts new file mode 100644 index 000000000..60fa37163 --- /dev/null +++ b/apps/api-gateway/src/dtos/apiResponse.dto copy.ts @@ -0,0 +1,15 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class ApiResponseDto { + @ApiProperty({ example: 'Success' }) + message: string; + + @ApiProperty() + success: boolean; + + @ApiProperty() + data?: any; + + @ApiProperty({ example: 200 }) + code?: number; +} diff --git a/apps/api-gateway/src/dtos/apiResponse.dto.ts b/apps/api-gateway/src/dtos/apiResponse.dto.ts new file mode 100644 index 000000000..4e15f1f71 --- /dev/null +++ b/apps/api-gateway/src/dtos/apiResponse.dto.ts @@ -0,0 +1,15 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class ApiResponseDto { + @ApiProperty({ example: 'Success' }) + message: string; + + @ApiProperty() + success: boolean; + + @ApiProperty() + data?: object; + + @ApiProperty({ example: 200 }) + statusCode?: number; +} diff --git a/apps/api-gateway/src/dtos/approval-status.dto.ts b/apps/api-gateway/src/dtos/approval-status.dto.ts new file mode 100644 index 000000000..a8113edbb --- /dev/null +++ b/apps/api-gateway/src/dtos/approval-status.dto.ts @@ -0,0 +1,7 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class ApprovalStatusDto { + + @ApiProperty() + approvalStatus: boolean; +} diff --git a/apps/api-gateway/src/dtos/authDto.dto.ts b/apps/api-gateway/src/dtos/authDto.dto.ts new file mode 100644 index 000000000..3b5b4e926 --- /dev/null +++ b/apps/api-gateway/src/dtos/authDto.dto.ts @@ -0,0 +1,21 @@ +import { IsBoolean, IsEmail, IsNotEmpty, IsOptional, IsString } from 'class-validator'; + +import { ApiProperty } from '@nestjs/swagger'; + +export class AuthDto { + @ApiProperty({ example: 'awqx@getnada.com' }) + @IsEmail() + @IsNotEmpty({ message: 'Please provide valid email' }) + @IsString({ message: 'email should be string' }) + email: string; + + @IsOptional() + @IsBoolean({ message: 'flag should be boolean' }) + flag: boolean; + + @IsOptional() + @ApiProperty({ example: 'Password@1' }) + @IsNotEmpty({ message: 'Please provide valid password' }) + @IsString({ message: 'password should be string' }) + password: string; +} diff --git a/apps/api-gateway/src/dtos/bad-request-error.dto.ts b/apps/api-gateway/src/dtos/bad-request-error.dto.ts new file mode 100644 index 000000000..8a1669de5 --- /dev/null +++ b/apps/api-gateway/src/dtos/bad-request-error.dto.ts @@ -0,0 +1,14 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { HttpStatus } from '@nestjs/common'; + +export class BadRequestErrorDto { + + @ApiProperty({ example: HttpStatus.BAD_REQUEST }) + statusCode: number; + + @ApiProperty({ example: 'Please provide valid data' }) + message: string; + + @ApiProperty({ example: 'Bad Request' }) + error: string; +} diff --git a/apps/api-gateway/src/dtos/category.dto.ts b/apps/api-gateway/src/dtos/category.dto.ts new file mode 100644 index 000000000..f3bfb7a6e --- /dev/null +++ b/apps/api-gateway/src/dtos/category.dto.ts @@ -0,0 +1,22 @@ +import { IsBoolean, IsNotEmpty, IsString, MaxLength, MinLength } from 'class-validator'; + +import { ApiProperty } from '@nestjs/swagger'; + +export class CategoryDto { + @ApiProperty() + @IsString({ message: 'name must be a string' }) + @IsNotEmpty({ message: 'please provide valid name' }) + name: string; + + @ApiProperty() + @IsString({ message: 'description must be a string' }) + @MaxLength(150) + @MinLength(2) + @IsNotEmpty({ message: 'please provide valid description' }) + description: string; + + @ApiProperty() + @IsNotEmpty({ message: 'Please provide a isActive' }) + @IsBoolean({ message: 'isActive id should be boolean' }) + isActive: boolean; +} diff --git a/apps/api-gateway/src/dtos/connection-out-of-band.dto.ts b/apps/api-gateway/src/dtos/connection-out-of-band.dto.ts new file mode 100644 index 000000000..bdfc6f1fc --- /dev/null +++ b/apps/api-gateway/src/dtos/connection-out-of-band.dto.ts @@ -0,0 +1,28 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsArray, IsString, IsNotEmpty, IsObject, IsNegative } from 'class-validator'; + + +interface attachmentsObject{ +id:string, +type:string +} + +export class ConnectionOutOfBandDto { + + @ApiProperty({'example':''}) + alias?:string; + + @ApiProperty({'example':'[{id:107ad6d1-5312-4b2b-bbfa-6becf6155e23,type:credential-offer}]'}) + @IsArray({message:'attachemnts must be in array'}) + @IsNotEmpty({message:'Please provide valid attachments'}) + attachments:attachmentsObject[]; + + @ApiProperty({'example':'["did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/didexchange/1.0"]'}) + handshake_protocols : string[]; + + @ApiProperty({'example':''}) + my_label?: string; + + @ApiProperty({'example': false}) + use_public_did : boolean; +} \ No newline at end of file diff --git a/apps/api-gateway/src/dtos/connection.dto.ts b/apps/api-gateway/src/dtos/connection.dto.ts new file mode 100644 index 000000000..3df7acb17 --- /dev/null +++ b/apps/api-gateway/src/dtos/connection.dto.ts @@ -0,0 +1,49 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class ConnectionDto { + + @ApiProperty() + // tslint:disable-next-line: variable-name + connection_id: string; + + @ApiProperty() + state: string; + + @ApiProperty() + // tslint:disable-next-line: variable-name + my_did: string; + + @ApiProperty() + // tslint:disable-next-line: variable-name + their_did: string; + + @ApiProperty() + // tslint:disable-next-line: variable-name + their_label: string; + + @ApiProperty() + initiator: string; + + @ApiProperty() + // tslint:disable-next-line: variable-name + invitation_key: string; + + @ApiProperty() + // tslint:disable-next-line: variable-name + routing_state: string; + + @ApiProperty() + accept: string; + + @ApiProperty() + // tslint:disable-next-line: variable-name + invitation_mode: string; + + @ApiProperty() + // tslint:disable-next-line: variable-name + updated_at: string; + + @ApiProperty() + // tslint:disable-next-line: variable-name + created_at: string; +} \ No newline at end of file diff --git a/apps/api-gateway/src/dtos/create-credential-definition.dto.ts b/apps/api-gateway/src/dtos/create-credential-definition.dto.ts new file mode 100644 index 000000000..bee72af3f --- /dev/null +++ b/apps/api-gateway/src/dtos/create-credential-definition.dto.ts @@ -0,0 +1,32 @@ +import { IsBoolean, IsDefined, IsNotEmpty, IsNumber, IsOptional, IsString } from 'class-validator'; + +import { ApiProperty } from '@nestjs/swagger'; + +export class CreateCredentialDefinitionDto { + + @ApiProperty({ 'example': 'default' }) + @IsNotEmpty({ message: 'Please provide a tag' }) + @IsString({ message: 'Tag id should be string' }) + tag: string; + + @ApiProperty({ 'example': 'WgWxqztrNooG92RXvxSTWv:2:schema_name:1.0' }) + @IsNotEmpty({ message: 'Please provide a schema id' }) + @IsString({ message: 'Schema id should be string' }) + schemaLedgerId: string; + + @ApiProperty() + @IsNumber() + @IsNotEmpty({ message: 'Please provide orgId' }) + orgId: number; + + @ApiProperty({ required: false }) + @IsOptional() + @IsString({ message: 'orgDid must be a string' }) + orgDid: string; + + @ApiProperty({ default: true }) + @IsDefined({ message: 'Revocable is required.' }) + @IsBoolean({ message: 'Revocable must be a boolean value.' }) + @IsNotEmpty({ message: 'Please provide whether the revocable must be true or false' }) + revocable = true; +} diff --git a/apps/api-gateway/src/dtos/create-feature-price.dto.ts b/apps/api-gateway/src/dtos/create-feature-price.dto.ts new file mode 100644 index 000000000..620fcc06d --- /dev/null +++ b/apps/api-gateway/src/dtos/create-feature-price.dto.ts @@ -0,0 +1,31 @@ +import { IsArray, IsInt, IsNotEmpty, IsNumberString, isInt, isNumber } from 'class-validator'; + +import { ApiProperty } from '@nestjs/swagger'; + +interface featurePriceData { + featureId: number, + featurePrice: number +} +export class CreateFeaturePriceDto { + + @ApiProperty({ 'example': 1 }) + @IsNotEmpty({ message: 'Please provide network id.' }) + @IsInt({ message: 'Please provide valid network id.' }) + networkID: number; + + @ApiProperty({ + 'example': [ +{ + featureId: 1, + featurePrice: 200 + }, + { + featureId: 2, + featurePrice: 400 + } +] + }) + @IsNotEmpty({ message: 'Please provide featureId and price.' }) + @IsArray({ message: 'FeatureId and price should be in array format.' }) + featurePrice: featurePriceData[]; +} diff --git a/apps/api-gateway/src/dtos/create-proof-request.dto.ts b/apps/api-gateway/src/dtos/create-proof-request.dto.ts new file mode 100644 index 000000000..ac5c5a4c4 --- /dev/null +++ b/apps/api-gateway/src/dtos/create-proof-request.dto.ts @@ -0,0 +1,20 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class CreateProofRequest { + @ApiProperty({'example': 'comments'}) + comment: string; + + @ApiProperty({ 'example': 'WgWxqztrNooG92RXvxSTWv:3:CL:20:tag' }) + credDefId?: string; + + @ApiProperty({ + 'example': [ +{ + attributeName: 'attributeName', + condition: '>=', + value: 'predicates' + } +] + }) + attributes: object[]; +} \ No newline at end of file diff --git a/apps/api-gateway/src/dtos/create-revocation-registry.dto.ts b/apps/api-gateway/src/dtos/create-revocation-registry.dto.ts new file mode 100644 index 000000000..efc0cc2b9 --- /dev/null +++ b/apps/api-gateway/src/dtos/create-revocation-registry.dto.ts @@ -0,0 +1,13 @@ +import { ApiProperty } from '@nestjs/swagger'; + + +export class CreateRevocationRegistryDto { + @ApiProperty({ example: 100 }) + max_cred_num: number; + + @ApiProperty({ example: true }) + issuance_by_default: boolean; + + @ApiProperty({ example: 'WgWxqztrNooG92RXvxSTWv:3:CL:20:tag' }) + credential_definition_id: string; +} \ 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 new file mode 100644 index 000000000..413f42063 --- /dev/null +++ b/apps/api-gateway/src/dtos/create-schema.dto.ts @@ -0,0 +1,29 @@ +import { IsArray, IsNotEmpty, IsNumber, IsOptional, IsString } from 'class-validator'; + +import { ApiProperty } from '@nestjs/swagger'; + +export class CreateSchemaDto { + @ApiProperty() + @IsString({ message: 'schema version must be a string' }) @IsNotEmpty({ message: 'please provide valid schema version' }) + schemaVersion: string; + + @ApiProperty() + @IsString({ message: 'schema name must be a string' }) @IsNotEmpty({ message: 'please provide valid schema name' }) + schemaName: string; + + @ApiProperty() + @IsArray({ message: 'attributes must be an array' }) + @IsString({ each: true }) + @IsNotEmpty({ message: 'please provide valid attributes' }) + attributes: string[]; + + @ApiProperty() + @IsNumber() + @IsNotEmpty({ message: 'please provide orgId' }) + orgId: number; + + @ApiProperty() + @IsOptional() + @IsString({ message: 'orgDid must be a string' }) + orgDid: string; +} diff --git a/apps/api-gateway/src/dtos/createEnterprise-query.dto.ts b/apps/api-gateway/src/dtos/createEnterprise-query.dto.ts new file mode 100644 index 000000000..37af0253f --- /dev/null +++ b/apps/api-gateway/src/dtos/createEnterprise-query.dto.ts @@ -0,0 +1,38 @@ +import {IsNotEmpty, IsString, IsOptional } from 'class-validator'; + +import { ApiProperty } from '@nestjs/swagger'; + +export class CreateEnterpriseQueryDto { + + @ApiProperty({ example: 'Name' }) + @IsNotEmpty({ message: 'Please provide valid first name' }) + @IsString({ message: 'First name should be a string' }) + firstName: string; + + @ApiProperty({ example: 'Last name' }) + @IsNotEmpty({ message: 'Please provide valid last name' }) + @IsString({ message: 'Last name should be a string' }) + lastName: string; + + @ApiProperty({ example: 'email@example.com' }) + @IsNotEmpty({ message: 'Please provide valid email address' }) + // @IsEmail({ message: 'Please provide valid email address' }) + emailAddress: string; + + @IsOptional() + @ApiProperty({ example: '1234567890' }) + mobileNumber: string; + + @IsOptional() + @ApiProperty({ example: 'Organization Name' }) + organizationName: string; + + @IsOptional() + @ApiProperty({ example: 'Role in organization' }) + roleInOrganization: string; + + @IsOptional() + @ApiProperty({ example: 'Your query' }) + query: string; + +} \ No newline at end of file diff --git a/apps/api-gateway/src/dtos/created-response-dto.ts b/apps/api-gateway/src/dtos/created-response-dto.ts new file mode 100644 index 000000000..3c3beeb36 --- /dev/null +++ b/apps/api-gateway/src/dtos/created-response-dto.ts @@ -0,0 +1,16 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { HttpStatus } from '@nestjs/common'; + +export class CreatedResponseDto { + @ApiProperty({ example: 'Created' }) + message: string; + + @ApiProperty() + success: boolean; + + @ApiProperty() + data?: any; + + @ApiProperty({ example: HttpStatus.CREATED }) + code?: number; +} diff --git a/apps/api-gateway/src/dtos/credential-offer.dto.ts b/apps/api-gateway/src/dtos/credential-offer.dto.ts new file mode 100644 index 000000000..c59e54406 --- /dev/null +++ b/apps/api-gateway/src/dtos/credential-offer.dto.ts @@ -0,0 +1,40 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsArray, IsNotEmpty, IsString } from "class-validator"; + +interface attributeValue { + name: string, + value: string, +} + +export class IssueCredentialOffer { + + @ApiProperty({ example: { 'protocolVersion': 'v1' } }) + @IsNotEmpty({ message: 'Please provide valid protocol-version' }) + @IsString({ message: 'protocol-version should be string' }) + protocolVersion: string; + + @ApiProperty({ example: { 'attributes': [{ 'value': 'string', 'name': 'string' }] } }) + @IsNotEmpty({ message: 'Please provide valid attributes' }) + @IsArray({ message: 'attributes should be array' }) + attributes: attributeValue[]; + + @ApiProperty({ example: { 'credentialDefinitionId': 'string' } }) + @IsNotEmpty({ message: 'Please provide valid credentialDefinitionId' }) + @IsString({ message: 'credentialDefinitionId should be string' }) + credentialDefinitionId: string; + + @ApiProperty({ example: { autoAcceptCredential: 'always' } }) + @IsNotEmpty({ message: 'Please provide valid autoAcceptCredential' }) + @IsString({ message: 'autoAcceptCredential should be string' }) + autoAcceptCredential: string; + + @ApiProperty({ example: { comment: 'string' } }) + @IsNotEmpty({ message: 'Please provide valid comment' }) + @IsString({ message: 'comment should be string' }) + comment: string; + + @ApiProperty({ example: { connectionId: '3fa85f64-5717-4562-b3fc-2c963f66afa6' } }) + @IsNotEmpty({ message: 'Please provide valid connection-id' }) + @IsString({ message: 'Connection-id should be string' }) + connectionId: string; +} diff --git a/apps/api-gateway/src/dtos/credential-send-offer.dto.ts b/apps/api-gateway/src/dtos/credential-send-offer.dto.ts new file mode 100644 index 000000000..1467ef6af --- /dev/null +++ b/apps/api-gateway/src/dtos/credential-send-offer.dto.ts @@ -0,0 +1,10 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsNotEmpty, IsString } from "class-validator"; + +export class CredentialSendOffer { + + @ApiProperty({ example: 'string' }) + @IsNotEmpty({ message: 'Please provide valid credentialRecordId' }) + @IsString({ message: 'credentialRecordId should be string' }) + credentialRecordId: string; +} diff --git a/apps/api-gateway/src/dtos/email.dto.ts b/apps/api-gateway/src/dtos/email.dto.ts new file mode 100644 index 000000000..457f050c5 --- /dev/null +++ b/apps/api-gateway/src/dtos/email.dto.ts @@ -0,0 +1,18 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class EmailDto { + @ApiProperty() + emailFrom: string; + + @ApiProperty() + emailTo: string; + + @ApiProperty() + emailSubject: string; + + @ApiProperty() + emailText: string; + + @ApiProperty() + emailHtml: string; +} \ No newline at end of file diff --git a/apps/api-gateway/src/dtos/endorse-transaction.dto.ts b/apps/api-gateway/src/dtos/endorse-transaction.dto.ts new file mode 100644 index 000000000..ea0b7c345 --- /dev/null +++ b/apps/api-gateway/src/dtos/endorse-transaction.dto.ts @@ -0,0 +1,6 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class EndorseTransactionDto { + @ApiProperty() + transactionId: string; +} diff --git a/apps/api-gateway/src/dtos/enums.ts b/apps/api-gateway/src/dtos/enums.ts new file mode 100644 index 000000000..ea8b3ebd9 --- /dev/null +++ b/apps/api-gateway/src/dtos/enums.ts @@ -0,0 +1,51 @@ +export enum OrgRequestStatus { + All = 'all', + Accepted = 'accepted', + Pending = 'pending', + Rejected = 'rejected', +} + +export enum ActiveFlags { + All = 'all', + True = 'true', + False = 'false', +} + +export enum NonAdminUserStae { + All = 'all', + Active = 'active', + InActive = 'inactive', + Pending = 'pending' +} + +export enum IssueCredStatus { + All = 'all', + Credential_issued = 'credential_issued', + Credential_revoke = 'credential_revoke', + Non_revoke = 'non_revoke' +} + +export enum AgentActions { + Start = 'START', + Stop = 'STOP', + Status = 'STATUS', +} + +export enum TenantStatus { + All = 'all', + Accepted = 'accepted', + Pending = 'pending', + Rejected = 'rejected', +} + +export enum OrgInvitationStatus { + All = 'all', + Accepted = 'accepted', + Pending = 'pending' +} + +export enum FileRecordStatus { + All = 'ALL', + Success = 'SUCCESS', + Error = 'ERROR' +} diff --git a/apps/api-gateway/src/dtos/fido-user.dto.ts b/apps/api-gateway/src/dtos/fido-user.dto.ts new file mode 100644 index 000000000..a0864cc04 --- /dev/null +++ b/apps/api-gateway/src/dtos/fido-user.dto.ts @@ -0,0 +1,136 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsArray, IsBoolean, IsEmail, IsNotEmpty, IsOptional, IsString, ValidateNested } from 'class-validator'; + +export class GenerateRegistrationDto { + @ApiProperty({ example: 'abc@vomoto.com' }) + @IsNotEmpty({ message: 'Email is required.' }) + @IsEmail() + userName: string; + + @IsOptional() + @ApiProperty({ example: 'false' }) + @IsBoolean({ message: 'isPasskey should be boolean' }) + deviceFlag: boolean; +} + +export class ResponseDto { + @ApiProperty() + @IsString() + attestationObject: string; + + @ApiProperty() + @IsString() + clientDataJSON: string; + + @ApiProperty() + @IsArray() + transports: string[]; +} + +export class ClientExtensionResultsDto { + @ValidateNested() + credProps: Record; +} + +export class VerifyRegistrationDto { + @ApiProperty() + @IsString() + id: string; + + @ApiProperty() + @IsString() + rawId: string; + + @ApiProperty({ type: ResponseDto, nullable: true }) + @IsOptional() + response: ResponseDto; + + @ApiProperty() + @IsString() + type: string; + + @ApiProperty() + clientExtensionResults: ClientExtensionResultsDto; + + @ApiProperty() + @IsString() + authenticatorAttachment: string; + + @ApiProperty() + @IsString() + challangeId: string; +} + +export class UpdateFidoUserDetailsDto { + @ApiProperty() + @IsString() + userName: string; + + @ApiProperty() + @IsString() + credentialId: string; + + @ApiProperty() + @IsString() + deviceFriendlyName: string; +} + +export class GenerateAuthenticationDto { + @ApiProperty({ example: 'abc@vomoto.com' }) + @IsString() + userName: string; +} + +class VerifyAuthenticationResponseDto { + @ApiProperty() + @IsString() + authenticatorData: string; + + @ApiProperty() + @IsString() + clientDataJSON: string; + + @ApiProperty() + @IsString() + signature: string; + + @ApiProperty() + @IsString() + userHandle: string; + } + + + export class VerifyAuthenticationDto { + @ApiProperty() + @IsString() + id: string; + + @ApiProperty() + @IsString() + rawId: string; + + @ApiProperty() + @IsOptional() + response: VerifyAuthenticationResponseDto; + + @ApiProperty() + @IsString() + type: string; + + @ApiProperty() + clientExtensionResults?: ClientExtensionResultsDto; + + @ApiProperty() + @IsString() + authenticatorAttachment: string; + + @ApiProperty() + @IsString() + challangeId: string; + } + + export class UserNameDto { + @ApiProperty() + @IsString() + userName: string; + } \ No newline at end of file diff --git a/apps/api-gateway/src/dtos/forbidden-error.dto copy.ts b/apps/api-gateway/src/dtos/forbidden-error.dto copy.ts new file mode 100644 index 000000000..d02512b73 --- /dev/null +++ b/apps/api-gateway/src/dtos/forbidden-error.dto copy.ts @@ -0,0 +1,10 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class ForbiddenErrorDto { + + @ApiProperty({ example: 403 }) + statusCode: number; + + @ApiProperty({ example: 'Forbidden' }) + error: string; +} diff --git a/apps/api-gateway/src/dtos/forbidden-error.dto.ts b/apps/api-gateway/src/dtos/forbidden-error.dto.ts new file mode 100644 index 000000000..d02512b73 --- /dev/null +++ b/apps/api-gateway/src/dtos/forbidden-error.dto.ts @@ -0,0 +1,10 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class ForbiddenErrorDto { + + @ApiProperty({ example: 403 }) + statusCode: number; + + @ApiProperty({ example: 'Forbidden' }) + error: string; +} diff --git a/apps/api-gateway/src/dtos/forgot-password.dto.ts b/apps/api-gateway/src/dtos/forgot-password.dto.ts new file mode 100644 index 000000000..c20ffb42c --- /dev/null +++ b/apps/api-gateway/src/dtos/forgot-password.dto.ts @@ -0,0 +1,17 @@ +import { IsEmail, IsNotEmpty, IsString } from 'class-validator'; + +import { ApiProperty } from '@nestjs/swagger'; + +export class ForgotPasswordDto { + + @ApiProperty({ example: 'awqx@getnada.com' }) + @IsNotEmpty({ message: 'Please provide valid username' }) + @IsString({ message: 'username should be string' }) + @IsEmail() + username: string; + + @ApiProperty() + @IsNotEmpty({ message: 'Please provide valid password' }) + @IsString({ message: 'password should be string' }) + password: string; +} \ No newline at end of file diff --git a/apps/api-gateway/src/dtos/get-cred-defs.dto.ts b/apps/api-gateway/src/dtos/get-cred-defs.dto.ts new file mode 100644 index 000000000..ab341ef7d --- /dev/null +++ b/apps/api-gateway/src/dtos/get-cred-defs.dto.ts @@ -0,0 +1,10 @@ +// import { SortValue } from '@credebl/enum/enum'; + +export class GetAllCredDefsDto { + pageSize?: number; + pageNumber?: number; + searchByText?: string; + sorting?: string; + revocable?: boolean; + sortByValue?: string; +} \ No newline at end of file diff --git a/apps/api-gateway/src/dtos/holder-details.dto.ts b/apps/api-gateway/src/dtos/holder-details.dto.ts new file mode 100644 index 000000000..bae2c8ead --- /dev/null +++ b/apps/api-gateway/src/dtos/holder-details.dto.ts @@ -0,0 +1,67 @@ +import { IsEmail, IsNotEmpty, IsString } from 'class-validator'; + +import { ApiProperty } from '@nestjs/swagger'; + +export class HolderDetailsDto { + @ApiProperty({ example: 'Alen' }) + @IsNotEmpty({ message: 'Please provide valid firstName' }) + @IsString({ message: 'firstName should be string' }) + firstName: string; + + @ApiProperty({ example: 'Harvey' }) + @IsNotEmpty({ message: 'Please provide valid lastName' }) + @IsString({ message: 'lastName should be string' }) + lastName: string; + + @ApiProperty({ example: 'awqx@example.com' }) + @IsEmail() + @IsNotEmpty({ message: 'Please provide valid email' }) + @IsString({ message: 'email should be string' }) + email: string; + + @ApiProperty({ example: 'awqx@example.com' }) + @IsEmail() + @IsNotEmpty({ message: 'Please provide valid username' }) + @IsString({ message: 'username should be string' }) + username: string; + + @ApiProperty({ example: 'your-secret-password' }) + @IsNotEmpty({ message: 'Please provide valid password' }) + @IsString({ message: 'password should be string' }) + password: string; + + // connectionId?: string; + + @ApiProperty({ example: 'GamoraPlus' }) + @IsNotEmpty({ message: 'Please provide valid deviceId' }) + @IsString({ message: 'deviceId should be string' }) + deviceId: string; + + @ApiProperty({ example: 'Nokia C3' }) + @IsNotEmpty({ message: 'Please provide valid model' }) + @IsString({ message: 'model should be string' }) + model: string; + + @ApiProperty({ example: 'Nokia' }) + @IsNotEmpty({ message: 'Please provide valid type' }) + @IsString({ message: 'type should be string' }) + type: string; + + @ApiProperty({ example: 'Android' }) + @IsNotEmpty({ message: 'Please provide valid os' }) + @IsString({ message: 'os should be string' }) + os: string; + + // @ApiProperty() + // mediatorId?: number; + + @ApiProperty({ example: 'https://yourdomain.com/my-profile-logo.png' }) + // @IsNotEmpty({ message: 'Please provide valid profileLogoUrl' }) + // @IsString({ message: 'profileLogoUrl should be string' }) + profileLogoUrl: string; + + @ApiProperty({ example: 'your-firebase-token' }) + @IsNotEmpty({ message: 'Please provide valid firebaseToken' }) + @IsString({ message: 'firebaseToken should be string' }) + firebaseToken: string; +} \ No newline at end of file diff --git a/apps/api-gateway/src/dtos/holder-reset-password.ts b/apps/api-gateway/src/dtos/holder-reset-password.ts new file mode 100644 index 000000000..c8424ea8d --- /dev/null +++ b/apps/api-gateway/src/dtos/holder-reset-password.ts @@ -0,0 +1,21 @@ +import { IsNotEmpty, IsString } from 'class-validator'; + +import { ApiProperty } from '@nestjs/swagger'; + +export class HolderResetPasswordDto { + @ApiProperty({ example: 'abc@getnada.com' }) + @IsNotEmpty({ message: 'Please provide valid email' }) + @IsString({ message: 'Email should be string' }) + email: string; + + @ApiProperty({ example: '$2b$10$.NcA4.oN.a8otc5TgGuO5OvH.hbaF/AWNvVfA1t7g3N9jstvzJTlm' }) + @IsNotEmpty({ message: 'Please provide valid password' }) + @IsString({ message: 'password should be string' }) + password: string; + + + @ApiProperty({ example: '647bf6c8b888b6a269ca' }) + @IsNotEmpty({ message: 'Please provide valid token' }) + @IsString({ message: 'token should be string' }) + token: string; +} \ No newline at end of file diff --git a/apps/api-gateway/src/dtos/internal-server-error-res.dto.ts b/apps/api-gateway/src/dtos/internal-server-error-res.dto.ts new file mode 100644 index 000000000..9b1d4afe5 --- /dev/null +++ b/apps/api-gateway/src/dtos/internal-server-error-res.dto.ts @@ -0,0 +1,10 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class InternalServerErrorDto { + + @ApiProperty({ example: 500 }) + statusCode: number; + + @ApiProperty({ example: 'Internal server error' }) + error: string; +} diff --git a/apps/api-gateway/src/dtos/issue-credential-offer.dto .ts b/apps/api-gateway/src/dtos/issue-credential-offer.dto .ts new file mode 100644 index 000000000..9eed1f5ed --- /dev/null +++ b/apps/api-gateway/src/dtos/issue-credential-offer.dto .ts @@ -0,0 +1,64 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsString, IsNotEmpty, IsBoolean, IsNotEmptyObject, IsObject } from 'class-validator'; +interface ICredAttrSpec { + 'mime-type': string, + name: string, + value: string +} + +interface ICredentialPreview { + '@type': string, + attributes: ICredAttrSpec[] +} + +export class IssueCredentialOfferDto { + + @ApiProperty({ example: true }) + @IsNotEmpty({message:'Please provide valid auto-issue'}) + @IsBoolean({message:'Auto-issue should be boolean'}) + auto_issue: boolean; + + @ApiProperty({ example: true }) + @IsNotEmpty({message:'Please provide valid auto-remove'}) + @IsBoolean({message:'Auto-remove should be boolean'}) + auto_remove: boolean; + + @ApiProperty({ example: 'comments' }) + @IsNotEmpty({message:'Please provide valid comment'}) + @IsString({message:'Comment should be string'}) + comment: string; + + @ApiProperty({ example: 'WgWxqztrNooG92RXvxSTWv:3:CL:20:tag' }) + @IsNotEmpty({message:'Please provide valid cred-def-id'}) + @IsString({message:'Cred-def-id should be string'}) + cred_def_id: string; + + @ApiProperty({ example: '3fa85f64-5717-4562-b3fc-2c963f66afa6' }) + @IsNotEmpty({message:'Please provide valid connection-id'}) + @IsString({message:'Connection-id should be string'}) + connection_id: string; + + @ApiProperty({ example: false }) + @IsNotEmpty({message:'Please provide valid trace'}) + @IsBoolean({message:'Trace should be boolean'}) + trace: boolean; + + + @ApiProperty({ + example: { + '@type': 'issue-credential/1.0/credential-preview', + 'attributes': [ + { + 'mime-type': 'image/jpeg', + 'name': 'favourite_drink', + 'value': 'martini' + } + ] + } + } + ) + + @IsObject({message:'Credential-preview should be object'}) + credential_preview: ICredentialPreview; +} + diff --git a/apps/api-gateway/src/dtos/issue-credential-out-of-band.dto.ts b/apps/api-gateway/src/dtos/issue-credential-out-of-band.dto.ts new file mode 100644 index 000000000..d412d28a8 --- /dev/null +++ b/apps/api-gateway/src/dtos/issue-credential-out-of-band.dto.ts @@ -0,0 +1,61 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsArray, IsString, IsNotEmpty, IsObject } from 'class-validator'; + +interface IssueCredAttrSpec { + 'mime-type': string, + name: string, + value: string +} + +interface IssueCredPreview { + '@type': string, + attributes: IssueCredAttrSpec[] +} + + +export class IssueCredentialOutOfBandDto { + + @ApiProperty({ example: 'WgWxqztrNooG92RXvxSTWv:3:CL:20:tag' }) + cred_def_id: string; + + @ApiProperty({example: { + '@type': 'issue-credential/1.0/credential-preview', + 'attributes': [ + { + 'mime-type': 'image/jpeg', + 'name': '', + 'value': 'i' + } + ] + } + }) + @IsObject({message:'credential_proposal must be a object'}) + @IsNotEmpty({message:'please provide valid credential_proposal'}) + credential_proposal: IssueCredPreview; + + @ApiProperty({example: 'WgWxqztrNooG92RXvxSTWv'}) + @IsString({message:'issuer_did must be a string'}) + @IsNotEmpty({message:'please provide valid issuer_did'}) + issuer_did: string; + + @ApiProperty({example:'WgWxqztrNooG92RXvxSTWv:2:schema_name:1.0'}) + @IsString({message:'schema_id must be a string'}) + @IsNotEmpty({message:'please provide valid schema_id'}) + schema_id: string; + + @ApiProperty({example:'WgWxqztrNooG92RXvxSTWv'}) + @IsString({message:'schema_iisuer_did name must be a string'}) + @IsNotEmpty({message:'please provide valid schema_issuer_did'}) + schema_issuer_did:string; + + @ApiProperty({example:'preferences'}) + @IsString({message:'schema name must be a string'}) + @IsNotEmpty({message:'please provide valid schema_name'}) + schema_name: string; + + @ApiProperty({example:'1.0'}) + @IsString({message:'schema version must be a string'}) + @IsNotEmpty({message:'please provide valid schema_version'}) + schema_version: string; + +} \ No newline at end of file diff --git a/apps/api-gateway/src/dtos/issue-credential-save.dto.ts b/apps/api-gateway/src/dtos/issue-credential-save.dto.ts new file mode 100644 index 000000000..0026cbfbd --- /dev/null +++ b/apps/api-gateway/src/dtos/issue-credential-save.dto.ts @@ -0,0 +1,69 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsOptional } from 'class-validator'; + +interface ITags { + state: string; + connectionId: string; + threadId: string; +} + +interface IndyCredential { + schemaId: string; + credentialDefinitionId: string; +} +interface ICredentialAttributes { + 'mime-type': string; + name: string; + value: string; +} + +interface IMetadata { + '_internal/indyCredential': IndyCredential; +} + +export class IssueCredentialSaveDto { + + @ApiProperty() + @IsOptional() + _tags: ITags; + + @ApiProperty() + @IsOptional() + metadata: IMetadata; + + @ApiProperty() + @IsOptional() + credentials: []; + + @ApiProperty() + @IsOptional() + id: string; + + @ApiProperty() + @IsOptional() + createdAt: string; + + @ApiProperty() + @IsOptional() + state: string; + + @ApiProperty() + @IsOptional() + connectionId: string; + + @ApiProperty() + @IsOptional() + threadId: string; + + @ApiProperty() + @IsOptional() + protocolVersion: string; + + @ApiProperty() + @IsOptional() + credentialAttributes: ICredentialAttributes[]; + + @ApiProperty() + @IsOptional() + autoAcceptCredential: string; +} \ No newline at end of file diff --git a/apps/api-gateway/src/dtos/issue-credential.dto.ts b/apps/api-gateway/src/dtos/issue-credential.dto.ts new file mode 100644 index 000000000..173c3731e --- /dev/null +++ b/apps/api-gateway/src/dtos/issue-credential.dto.ts @@ -0,0 +1,40 @@ +import { IsArray, IsNotEmpty, IsString } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; +import { toLowerCase, trim } from '@credebl/common/cast.helper'; +import { Transform } from 'class-transformer'; + +interface attribute { + name: string; + value: string; +} + +export class IssueCredentialDto { + + + @ApiProperty({ example: 'v1' }) + @Transform(({ value }) => trim(value)) + @Transform(({ value }) => toLowerCase(value)) + @IsNotEmpty({ message: 'Please provide valid protocolVersion' }) + @IsString({ message: 'protocolVersion should be string' }) + protocolVersion: string; + + @ApiProperty({ example: [{ 'value': 'string', 'name': 'string' }] }) + @IsNotEmpty({ message: 'Please provide valid attributes' }) + @IsArray({ message: 'attributes should be array' }) + attributes: attribute[]; + + @ApiProperty({ example: 'string' }) + @IsNotEmpty({ message: 'Please provide valid credentialDefinitionId' }) + @IsString({ message: 'credentialDefinitionId should be string' }) + credentialDefinitionId: string; + + @ApiProperty({ example: 'string' }) + @IsNotEmpty({ message: 'Please provide valid comment' }) + @IsString({ message: 'comment should be string' }) + comment: string; + + @ApiProperty({ example: '3fa85f64-5717-4562-b3fc-2c963f66afa6' }) + @IsNotEmpty({ message: 'Please provide valid connectionId' }) + @IsString({ message: 'connectionId should be string' }) + connectionId: string; +} \ No newline at end of file diff --git a/apps/api-gateway/src/dtos/label-editor.dto.ts b/apps/api-gateway/src/dtos/label-editor.dto.ts new file mode 100644 index 000000000..ceac8fb0f --- /dev/null +++ b/apps/api-gateway/src/dtos/label-editor.dto.ts @@ -0,0 +1,12 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class LabelEditorDto { + @ApiProperty() + labelId : number; + + @ApiProperty() + labelName?: string; + + @ApiProperty() + description?: string; +} \ No newline at end of file diff --git a/apps/api-gateway/src/dtos/not-found-error.dto.ts b/apps/api-gateway/src/dtos/not-found-error.dto.ts new file mode 100644 index 000000000..3ea199978 --- /dev/null +++ b/apps/api-gateway/src/dtos/not-found-error.dto.ts @@ -0,0 +1,22 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class NotFoundErrorDto { + + @ApiProperty({ example: 404 }) + statusCode: number; + + @ApiProperty({ example: 'Not Found' }) + error: string; + + @ApiProperty({ example: 'Not Found' }) + message: string; + + @ApiProperty() + success: boolean; + + @ApiProperty() + data?: boolean | {} | []; + + @ApiProperty({ example: 404 }) + code: number; +} diff --git a/apps/api-gateway/src/dtos/org-name-check.dto.ts b/apps/api-gateway/src/dtos/org-name-check.dto.ts new file mode 100644 index 000000000..2a980aea5 --- /dev/null +++ b/apps/api-gateway/src/dtos/org-name-check.dto.ts @@ -0,0 +1,16 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; + +export class OrgNameCheckDto { + @ApiProperty({example:'Organization name'}) + @IsNotEmpty({message:'Please provide valid organization Name'}) + @IsString({message:'Organization Name should be string'}) + orgName: string; + + @IsOptional() + @ApiPropertyOptional({example:'1'}) + @IsNotEmpty({message:'Please provide valid id of organization'}) + // @IsNumberString({message:'Organization id should be number'}) + orgId?: number; + +} \ No newline at end of file diff --git a/apps/api-gateway/src/dtos/platform-config.dto.ts b/apps/api-gateway/src/dtos/platform-config.dto.ts new file mode 100644 index 000000000..dfaed867e --- /dev/null +++ b/apps/api-gateway/src/dtos/platform-config.dto.ts @@ -0,0 +1,21 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class PlatformConfigDto { + @ApiProperty() + externalIP: string; + + @ApiProperty() + genesisURL: string; + + @ApiProperty() + lastInternalIP: string; + + @ApiProperty() + sgUsername: string; + + @ApiProperty() + sgApiKey: string; + + @ApiProperty() + sgEmailFrom: string; +} diff --git a/apps/api-gateway/src/dtos/platform-connection.dtos.ts b/apps/api-gateway/src/dtos/platform-connection.dtos.ts new file mode 100644 index 000000000..c4ab7f114 --- /dev/null +++ b/apps/api-gateway/src/dtos/platform-connection.dtos.ts @@ -0,0 +1,27 @@ +import { IsString, IsNotEmpty, IsInt } from 'class-validator'; +// export class ConnectionDto { +// @IsNotEmpty() +// @IsNumberString() +// itemsPerPage: number; + + // @IsNumber() + // page: number; + + // @IsString() + // searchText: string; + + // @IsNotEmpty({message:"Please provide valid org Id"}) + // @IsNumber() + // orgId: number + + // @IsString() + // connectionSortBy: string + + // @IsString() + // sortValue: string +// } + +export class Org { + @IsInt({message:'number expected'}) + orgId: number; +} \ No newline at end of file diff --git a/apps/api-gateway/src/dtos/printable-form-details.dto.ts b/apps/api-gateway/src/dtos/printable-form-details.dto.ts new file mode 100644 index 000000000..50bead236 --- /dev/null +++ b/apps/api-gateway/src/dtos/printable-form-details.dto.ts @@ -0,0 +1,12 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class PrintableFormDto { + @ApiProperty() + credDefId: string; + + @ApiProperty() + schemaId: string; + + @ApiProperty() + theirDid: string; +} \ No newline at end of file diff --git a/apps/api-gateway/src/dtos/register-non-admin-user.dto.ts b/apps/api-gateway/src/dtos/register-non-admin-user.dto.ts new file mode 100644 index 000000000..b894d4c7b --- /dev/null +++ b/apps/api-gateway/src/dtos/register-non-admin-user.dto.ts @@ -0,0 +1,40 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IsArray, IsInt, IsNotEmpty, IsString } from 'class-validator'; + +export class RegisterNonAdminUserDto { + @ApiProperty() + @IsNotEmpty({message:'Please provide valid firstName'}) + @IsString({message:'FirstName should be string'}) + firstName: string; + + @ApiProperty() + @IsNotEmpty({message:'Please provide valid lastName'}) + @IsString({message:'LastName should be string'}) + lastName: string; + + @ApiProperty() + @IsNotEmpty({message:'Please provide valid email'}) + @IsString({message:'Email should be string'}) + email: string; + + @ApiPropertyOptional() + // @IsNotEmpty({message:'Please provide valid username'}) + // @IsString({message:'Username should be string'}) + username: string; + + @ApiProperty() + @IsNotEmpty({message:'Please provide valid password'}) + @IsString({message:'Password should be string'}) + password: string; + + @ApiProperty() + @IsNotEmpty({message:'Role should be array of number'}) + @IsArray({message:'Role should be number'}) + @IsInt({ each: true }) + + // @IsInt({message:'Role should be number'}) + role:number; + + featureId: number; + +} diff --git a/apps/api-gateway/src/dtos/register-tenant.dto.ts b/apps/api-gateway/src/dtos/register-tenant.dto.ts new file mode 100644 index 000000000..d6e53675c --- /dev/null +++ b/apps/api-gateway/src/dtos/register-tenant.dto.ts @@ -0,0 +1,81 @@ +/* eslint-disable camelcase */ +import { ApiExtraModels, ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IsEmail, IsNotEmpty, IsNumberString, IsString, MaxLength, MinLength } from 'class-validator'; + +import { Transform } from 'class-transformer'; +import { toLowerCase, trim } from '@credebl/common/cast.helper'; + + +@ApiExtraModels() +export class RegisterTenantDto { + + @ApiProperty() + @Transform(({ value }) => trim(value)) + @Transform(({ value }) => toLowerCase(value)) + @IsNotEmpty({ message: 'Email is required.' }) + @MaxLength(256, { message: 'Email must be at most 256 character.' }) + @IsEmail() + email: string; + + @ApiProperty() + @Transform(({ value }) => trim(value)) + @IsNotEmpty({ message: 'Password is required.' }) + password: string; + + @ApiProperty() + @Transform(({ value }) => trim(value)) + @IsNotEmpty({ message: 'Organization name is required.' }) + @MinLength(2, { message: 'Organization name must be at least 2 characters.' }) + @MaxLength(50, { message: 'Organization name must be at most 50 characters.' }) + @IsString({ message: 'Organization name must be in string format.' }) + orgName: string; + + @ApiProperty() + @Transform(({ value }) => trim(value)) + @IsNotEmpty({ message: 'First name is required.' }) + @MinLength(1, { message: 'First name must be at least 2 characters.' }) + @MaxLength(50, { message: 'First name must be at most 50 characters.' }) + @IsString({ message: 'First name must be in string format.' }) + firstName: string; + + @ApiProperty() + @Transform(({ value }) => trim(value)) + @IsNotEmpty({ message: 'Last name is required.' }) + @MinLength(1, { message: 'Last name must be at least 2 characters.' }) + @MaxLength(50, { message: 'Last name must be at most 50 characters.' }) + @IsString({ message: 'Last name must be in string format.' }) + lastName: string; + + @ApiProperty() + @IsNotEmpty({ message: 'Organization categoty is required.' }) + @IsNumberString() + orgCategory: number; + + @ApiPropertyOptional() + logoUri?: string; + + + @ApiPropertyOptional() + @Transform(({ value }) => trim(value)) + solutionTitle?: string; + + @ApiPropertyOptional() + @Transform(({ value }) => trim(value)) + solutionDesc?: string; + + @ApiPropertyOptional() + @Transform(({ value }) => trim(value)) + address?: string; + + @ApiPropertyOptional() + @Transform(({ value }) => trim(value)) + website?: string; + + @ApiPropertyOptional() + @Transform(({ value }) => trim(value)) + tags?: string; + + @ApiPropertyOptional() + @Transform(({ value }) => trim(value)) + Keywords: string; +} diff --git a/apps/api-gateway/src/dtos/register-user.dto.ts b/apps/api-gateway/src/dtos/register-user.dto.ts new file mode 100644 index 000000000..cebeae3a4 --- /dev/null +++ b/apps/api-gateway/src/dtos/register-user.dto.ts @@ -0,0 +1,110 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IsInt, IsNotEmpty, IsOptional, IsString } from 'class-validator'; + +export class RegisterUserDto { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + attribute: any; + + + @ApiProperty() + @IsNotEmpty({message:'Please provide valid adminEmail'}) + @IsString({message:'AdminEmail should be string'}) + adminEmail: string; + + @ApiProperty() + @IsNotEmpty({message:'Please provide valid orgName'}) + @IsString({message:'OrgName should be string'}) + orgName: string; + + @ApiProperty() + // @IsNotEmpty({message:'Please provide valid adminPassword'}) + @IsString({message:'AdminPassword should be string'}) + adminPassword?: string; + + @ApiProperty() + @IsNotEmpty({message:'Please provide valid orgCategory'}) + // @IsInt({message:'OrgCategory should be number'}) + orgCategory: number; + + @ApiPropertyOptional() + @IsOptional() + @IsString({message:'FirstName should be string'}) + firstName: string; + + @ApiPropertyOptional() + @IsOptional() + @IsString({message:'LastName should be string'}) + lastName: string; + + @ApiPropertyOptional() + @IsOptional() + @IsString({message:'Description should be string'}) + description: string; + + @ApiPropertyOptional() + @IsOptional() + @IsString({message:'DisplayName should be string'}) + displayName: string; + + @ApiPropertyOptional() + @IsOptional() + @IsString({message:'LogoUrl should be string'}) + logoUrl: string; + + @ApiPropertyOptional() + @IsOptional() + @IsString({message:'Address should be string'}) + address: string; + + @ApiPropertyOptional() + @IsOptional() + @IsString({message:'AdminContact should be string'}) + adminContact: string; + + @ApiPropertyOptional() + @IsOptional() + // @IsInt({message:'NoOfUsers should be number'}) + noOfUsers: number; + + @ApiPropertyOptional() + @IsOptional() + // @IsInt({message:'NoOfSchemas should be number'}) + noOfSchemas: number; + + @ApiPropertyOptional() + @IsOptional() + // @IsInt({message:'NoOfCredentials should be number'}) + noOfCredentials: number; + + @ApiPropertyOptional() + @IsOptional() + @IsString({message:'AdminUsername should be string'}) + adminUsername: string; + + @ApiPropertyOptional() + @IsOptional() + @IsString({message:'Keywords should be string'}) + Keywords: string; + + @IsOptional() + @IsString({message:'ByAdmin should be string'}) + byAdmin?: string; + + @IsOptional() + // @IsInt({message:'TenantId should be number'}) + tenantId?: number; + + @IsOptional() + @IsString({message:'Tags should be string'}) + tags?: string; + + @IsOptional() + // @IsInt({message:'InviteId should be number'}) + inviteId: number; + + @IsOptional() + @IsInt({message:'OnBoardingType should be number'}) + onBoardingType?: number; + + featureId: number; +} diff --git a/apps/api-gateway/src/dtos/remote-get-credential.dto.ts b/apps/api-gateway/src/dtos/remote-get-credential.dto.ts new file mode 100644 index 000000000..7a14ea484 --- /dev/null +++ b/apps/api-gateway/src/dtos/remote-get-credential.dto.ts @@ -0,0 +1,16 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class GetRemoteCredentials { + + @ApiProperty({ example: 'WgWxqztrNooG92RXvxSTWv' }) + pairwiseDid: string; + + @ApiProperty({ example: 'WgWxqztrNooG92RXvxSTWv:3:CL:20:tag' }) + credDefId: string; + + @ApiProperty() + holderId: string; + + @ApiProperty() + verifierId: string; +} \ No newline at end of file diff --git a/apps/api-gateway/src/dtos/remove-holder.dto.ts b/apps/api-gateway/src/dtos/remove-holder.dto.ts new file mode 100644 index 000000000..11ebbc04f --- /dev/null +++ b/apps/api-gateway/src/dtos/remove-holder.dto.ts @@ -0,0 +1,13 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsString, IsNotEmpty } from 'class-validator'; +export class RemoveHolderDto { + @ApiProperty() + @IsNotEmpty({message:'Please provide valid username'}) + @IsString({message:'Username should be string'}) + username: string; + + @ApiProperty() + @IsNotEmpty({message:'Please provide valid password'}) + @IsString({message:'Password should be string'}) + password: string; +} diff --git a/apps/api-gateway/src/dtos/reset-password.dto.ts b/apps/api-gateway/src/dtos/reset-password.dto.ts new file mode 100644 index 000000000..44bd82b66 --- /dev/null +++ b/apps/api-gateway/src/dtos/reset-password.dto.ts @@ -0,0 +1,18 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsString, IsNotEmpty } from 'class-validator'; +export class ResetPasswordDto { + @ApiProperty() + @IsNotEmpty({message:'Please provide valid email'}) + @IsString({message:'Email should be string'}) + email: string; + + @ApiProperty() + @IsNotEmpty({message:'Please provide valid oldPassword'}) + @IsString({message:'oldPassword should be string'}) + oldPassword: string; + + @ApiProperty() + @IsNotEmpty({message:'Please provide valid newPassword'}) + @IsString({message:'newPassword should be string'}) + newPassword: string; +} \ No newline at end of file diff --git a/apps/api-gateway/src/dtos/revoke-credential.dto.ts b/apps/api-gateway/src/dtos/revoke-credential.dto.ts new file mode 100644 index 000000000..305ec6d10 --- /dev/null +++ b/apps/api-gateway/src/dtos/revoke-credential.dto.ts @@ -0,0 +1,36 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsString, IsNotEmpty, IsBoolean, IsNumber } from 'class-validator'; +export class RevokeCredentialDto { + + @ApiProperty({ example: 1 }) + @IsNotEmpty({message:'Please provide valid cred-rev-id'}) + @IsNumber() + cred_rev_id: number; + + @ApiProperty({ example: true }) + @IsNotEmpty({message:'Please provide valid publish'}) + @IsBoolean({message:'Publish should be boolean'}) + publish?: boolean; + + @ApiProperty({ example: 'Th7MpTaRZVRYnPiabds81Y:4:Th7MpTaRZVRYnPiabds81Y:3:CL:185:aadhar1:CL_ACCUM:0296a307-9127-481f-ba4f-c43f89f1420e' }) + @IsNotEmpty({message:'Please provide valid rev-reg-id'}) + @IsString({message:'Rev-reg-id should be string'}) + rev_reg_id: string; + + @ApiProperty() + @IsNotEmpty({message:'Please provide valid cred-ex-id'}) + @IsString({message:'Cred-ex-id should be string'}) + cred_ex_id?: string; + + @ApiProperty() + @IsNotEmpty({message:'Please provide valid email'}) + @IsString({message:'Email should be string'}) + email: string; + + @ApiProperty() + @IsNotEmpty({message:'Please provide valid password'}) + @IsString({message:'Password should be string'}) + password: string; + + featureId: number; +} \ No newline at end of file diff --git a/apps/api-gateway/src/dtos/save-roles-permissions.dto.ts b/apps/api-gateway/src/dtos/save-roles-permissions.dto.ts new file mode 100644 index 000000000..2f6595645 --- /dev/null +++ b/apps/api-gateway/src/dtos/save-roles-permissions.dto.ts @@ -0,0 +1,14 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class RolesPermissionsObj { + @ApiProperty() + roleId: number; + + @ApiProperty({ type: [] }) + permissionsId: number; +} + +export class SaveRolesPermissionsDto { + @ApiProperty({ type: [] }) + data: RolesPermissionsObj; +} diff --git a/apps/api-gateway/src/dtos/schemasearch.dto.ts b/apps/api-gateway/src/dtos/schemasearch.dto.ts new file mode 100644 index 000000000..36e578d4a --- /dev/null +++ b/apps/api-gateway/src/dtos/schemasearch.dto.ts @@ -0,0 +1,37 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class SchemaSearchDto { + @ApiProperty() + // tslint:disable-next-line: variable-name + schema_version?: string; + + @ApiProperty() + // tslint:disable-next-line: variable-name + schema_name?: string; + + @ApiProperty() + attributes?: string; + + @ApiProperty() + // tslint:disable-next-line: variable-name + schema_ledger_id?: string; + + @ApiProperty() + // tslint:disable-next-line: variable-name + issuer_did?: string; + + + // tslint:disable-next-line: variable-name + @ApiProperty() + search_text: string; + + // tslint:disable-next-line: variable-name + @ApiProperty() + items_per_page: number; + + @ApiProperty() + page: number; + + @ApiProperty() + filter_value : boolean; +} diff --git a/apps/api-gateway/src/dtos/send-invite-toOrg.dto.ts b/apps/api-gateway/src/dtos/send-invite-toOrg.dto.ts new file mode 100644 index 000000000..6c6283ddb --- /dev/null +++ b/apps/api-gateway/src/dtos/send-invite-toOrg.dto.ts @@ -0,0 +1,25 @@ +import { IsArray, IsNotEmpty, IsString } from 'class-validator'; + +import { ApiProperty } from '@nestjs/swagger'; + +export class SendInviteToOrgDto { + + @ApiProperty({ 'example': '[{"orgName":"xyz","orgEmail":"xyz@gmail.com","orgRole": 1}]' }) + @IsArray({ message: 'attributes must be an array' }) + // @IsString({ each: true }) + @IsNotEmpty({ message: 'please provide valid attributes' }) + emails : InvitationEmailIds[]; + + @ApiProperty() + @IsString({ message: 'description must be a string' }) + description :string; + +} + +export class InvitationEmailIds { + + orgName : string; + orgEmail : string; + orgRole : number[]; + +} \ No newline at end of file diff --git a/apps/api-gateway/src/dtos/send-proof-request.dto.ts b/apps/api-gateway/src/dtos/send-proof-request.dto.ts new file mode 100644 index 000000000..cc2416b1c --- /dev/null +++ b/apps/api-gateway/src/dtos/send-proof-request.dto.ts @@ -0,0 +1,26 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsArray, IsString, IsNotEmpty, IsObject, IsOptional } from 'class-validator'; + + +export class SendProofRequest { + @ApiProperty({ 'example': '3fa85f64-5717-4562-b3fc-2c963f66afa6' }) + @IsString({message:'connection id must be string'}) + @IsNotEmpty({message:'please provide valid connection Id'}) + connectionId: string; + + @ApiProperty({ + 'example': [ +{ + attributeName: 'attributeName', + condition: '>=', + value: 'predicates', + credDefId: '', + credentialName:'' + } +] + }) + @IsArray({message:'attributes must be in array'}) + @IsObject({each:true}) + @IsNotEmpty({message:'please provide valid attributes'}) + attributes: object[]; +} \ No newline at end of file diff --git a/apps/api-gateway/src/dtos/subscription.dto.ts b/apps/api-gateway/src/dtos/subscription.dto.ts new file mode 100644 index 000000000..857000553 --- /dev/null +++ b/apps/api-gateway/src/dtos/subscription.dto.ts @@ -0,0 +1,10 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class SubscriptionDto { + + @ApiProperty() + name: string; + + @ApiProperty() + description: string; +} diff --git a/apps/api-gateway/src/dtos/unauthorized-error.dto.ts b/apps/api-gateway/src/dtos/unauthorized-error.dto.ts new file mode 100644 index 000000000..5269ddd5c --- /dev/null +++ b/apps/api-gateway/src/dtos/unauthorized-error.dto.ts @@ -0,0 +1,10 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class UnauthorizedErrorDto { + + @ApiProperty({ example: 401 }) + statusCode: number; + + @ApiProperty({ example: 'Unauthorized' }) + error: string; +} diff --git a/apps/api-gateway/src/dtos/update-profile.dto.ts b/apps/api-gateway/src/dtos/update-profile.dto.ts new file mode 100644 index 000000000..ba1bc070b --- /dev/null +++ b/apps/api-gateway/src/dtos/update-profile.dto.ts @@ -0,0 +1,21 @@ +import { IsNotEmpty, IsString } from 'class-validator'; + +import { ApiProperty } from '@nestjs/swagger'; + +export class UpdateProfileDto { + + @ApiProperty() + @IsNotEmpty({message:'Please provide valid firstName'}) + @IsString({message:'FirstName should be string'}) + firstName: string; + + @ApiProperty() + @IsNotEmpty({message:'Please provide valid lastName'}) + @IsString({message:'LastName should be string'}) + lastName: string; + + @ApiProperty() + @IsNotEmpty({message:'Please provide valid profileLogoUrl'}) + @IsString({message:'ProfileLogoUrl should be string'}) + profileLogoUrl?: string; +} diff --git a/apps/api-gateway/src/dtos/update-revocation-registry.dto.ts b/apps/api-gateway/src/dtos/update-revocation-registry.dto.ts new file mode 100644 index 000000000..1449172a0 --- /dev/null +++ b/apps/api-gateway/src/dtos/update-revocation-registry.dto.ts @@ -0,0 +1,10 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class UpdateRevocationRegistryUriDto { + @ApiProperty({ 'example': 'WgWxqztrNooG92RXvxSTWv:4:WgWxqztrNooG92RXvxSTWv:3:CL:20:tag:CL_ACCUM:0' }) + // tslint:disable-next-line: variable-name + revoc_reg_id?: string; + @ApiProperty({ 'example': 'http://192.168.56.133:5000/revocation/registry/WgWxqztrNooG92RXvxSTWv:4:WgWxqztrNooG92RXvxSTWv:3:CL:20:tag:CL_ACCUM:0/tails-file' }) + // tslint:disable-next-line: variable-name + path?: string; +} \ No newline at end of file diff --git a/apps/api-gateway/src/dtos/user-counts.dto.ts b/apps/api-gateway/src/dtos/user-counts.dto.ts new file mode 100644 index 000000000..106178fa5 --- /dev/null +++ b/apps/api-gateway/src/dtos/user-counts.dto.ts @@ -0,0 +1,9 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class UserCountsDto { + @ApiProperty() + totalUser: number; + + @ApiProperty() + activeUser: number; +} diff --git a/apps/api-gateway/src/dtos/user-profile-update.dto.ts b/apps/api-gateway/src/dtos/user-profile-update.dto.ts new file mode 100644 index 000000000..1b161c5b6 --- /dev/null +++ b/apps/api-gateway/src/dtos/user-profile-update.dto.ts @@ -0,0 +1,14 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsString, IsNotEmpty } from 'class-validator'; + +export class UserOrgProfileDto { + @ApiProperty() + @IsNotEmpty({message:'Please provide valid firstName'}) + @IsString({message:'FirstName should be string'}) + firstName?: string; + + @ApiProperty() + @IsNotEmpty({message:'Please provide valid lastName'}) + @IsString({message:'LastName should be string'}) + lastName?: string; +} \ No newline at end of file diff --git a/apps/api-gateway/src/dtos/user-role-org-perms.dto.ts b/apps/api-gateway/src/dtos/user-role-org-perms.dto.ts new file mode 100644 index 000000000..61bfe49c2 --- /dev/null +++ b/apps/api-gateway/src/dtos/user-role-org-perms.dto.ts @@ -0,0 +1,18 @@ +export class UserRoleOrgPermsDto { + id :number; + role : userRoleDto; + Organization: userOrgDto; +} + +export class userRoleDto { + id: number; + name : string; + permissions :string[]; + +} + +export class userOrgDto { + id: number; + orgName :string; +} + diff --git a/apps/api-gateway/src/dtos/wallet-details.dto.ts b/apps/api-gateway/src/dtos/wallet-details.dto.ts new file mode 100644 index 000000000..45f2fc03c --- /dev/null +++ b/apps/api-gateway/src/dtos/wallet-details.dto.ts @@ -0,0 +1,17 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class WalletDetailsDto { + + @ApiProperty() + walletName: string; + + @ApiProperty() + walletPassword: string; + + @ApiProperty() + ledgerId: number; + + @ApiProperty() + transactionApproval?: string; + +} \ No newline at end of file diff --git a/apps/api-gateway/src/enum.ts b/apps/api-gateway/src/enum.ts new file mode 100644 index 000000000..0cfdecf91 --- /dev/null +++ b/apps/api-gateway/src/enum.ts @@ -0,0 +1,118 @@ +export enum sortValue { + ASC = 'ASC', + DESC = 'DESC', +} +export enum SortValue { + ASC = 'ASC', + DESC = 'DESC', +} +export enum onboardRequestSort { + id = 'id', + orgName = 'orgName', + createDateTime = 'createDateTime', + isEmailVerified = 'isEmailVerified', + lastChangedDateTime = 'lastChangedDateTime' +} + +export enum schemaSortBy { + id = 'id', + schemaName = 'schemaName', + createDateTime = 'createDateTime' +} + +export enum credDefSortBy { + id = 'id', + createDateTime = 'createDateTime', + tag = 'tag' +} + +export enum connectionSortBy { + id = 'id', + theirLabel = 'theirLabel', + createDateTime = 'createdAt' +} +export enum credentialSortBy { + id = 'id', + createDateTime = 'createDateTime' +} + +// export enum credRevokeStatus { +// all = 'all', +// revoke = 'revoke', +// notRevoke = 'notRevoke' +// } + +export enum booleanStatus { + all = 'all', + true = 'true', + false = 'false' +} + +export enum orgPresentProofRequestsSort { + id = 'id', + holderName = 'theirLabel', + createDateTime = 'createDateTime' +} + +export enum orgHolderRequestsSort { + id = 'id', + holderName = 'theirLabel', + createDateTime = 'createDateTime' +} + +export enum holderProofRequestsSort { + id = 'id', + createDateTime = 'createDateTime' +} + + +export enum OnboardRequestSort { + id = 'id', + createDateTime = 'createDateTime', + orgName = 'orgName' +} + +export enum CategorySortBy { + id = 'id', + Name = 'name' +} +export enum CredDefSortBy { + id = 'id', + createDateTime = 'createDateTime', + tag = 'tag' +} +export enum transactionSort { + id = 'id', + createDateTime = 'createDateTime', +} + +export enum ConnectionAlias { + endorser = 'ENDORSER', + author = 'AUTHOR', +} + +export enum TransactionRole { + transactionAuthor = 'TRANSACTION_AUTHOR', + transactionEndorser = 'TRANSACTION_ENDORSER', +} + +export enum TransactionType { + schema = 'SCHEMA', + credDef = 'CREDENTIAL_DEF', +} + +export enum OrderBy { + ASC = 'ASC', + DESC = 'DESC', +} + +export enum EmailAuditOrderByColumns { + CreatedDateTime = 'createDateTime', + Id = 'id', +} + +export enum ExpiredSubscriptionSortBy { + startDate = 'startDate', + endDate = 'endDate', + id = 'id', +} \ No newline at end of file diff --git a/apps/api-gateway/src/fido/dto/fido-user.dto.ts b/apps/api-gateway/src/fido/dto/fido-user.dto.ts new file mode 100644 index 000000000..46069d7ed --- /dev/null +++ b/apps/api-gateway/src/fido/dto/fido-user.dto.ts @@ -0,0 +1,103 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsBoolean, IsEmail, IsNotEmpty, IsOptional } from 'class-validator'; + + +export class GenerateRegistrationDto { + @ApiProperty({ example: 'abc@vomoto.com' }) + @IsNotEmpty({ message: 'Email is required.' }) + @IsEmail() + userName: string; + + @IsOptional() + @ApiProperty({ example: 'false' }) + @IsBoolean({ message: 'isPasskey should be boolean' }) + deviceFlag: boolean; +} + +export class VerifyRegistrationDto { + @ApiProperty() + id: string; + + @ApiProperty() + rawId: string; + + @ApiProperty() + response: Response; + + @ApiProperty() + type: string; + + @ApiProperty() + clientExtensionResults: ClientExtensionResults; + + @ApiProperty() + authenticatorAttachment: string; + + @ApiProperty() + challangeId: string; +} + +export interface Response { + attestationObject: string + clientDataJSON: string + transports: [] +} + +export interface ClientExtensionResults { + credProps: CredProps +} + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface CredProps { } + + +export class VerifyAuthenticationDto { + @ApiProperty() + id: string; + + @ApiProperty() + rawId: string; + + @ApiProperty() + response: Response; + + @ApiProperty() + type: string; + + @ApiProperty() + clientExtensionResults: ClientExtensionResults; + + @ApiProperty() + authenticatorAttachment: string; + + @ApiProperty() + challangeId: string; +} + +export interface Response { + authenticatorData: string + clientDataJSON: string + signature: string + userHandle: string +} + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface ClientExtensionResults { } + + +export class UpdateFidoUserDetailsDto { + @ApiProperty() + userName: string; + + @ApiProperty() + credentialId: string; + + @ApiProperty() + deviceFriendlyName: string; + +} + +export class GenerateAuthenticationDto { + @ApiProperty({ example: 'abc@vomoto.com' }) + userName: string; +} diff --git a/apps/api-gateway/src/fido/fido.controller.ts b/apps/api-gateway/src/fido/fido.controller.ts new file mode 100644 index 000000000..ace7dbfed --- /dev/null +++ b/apps/api-gateway/src/fido/fido.controller.ts @@ -0,0 +1,229 @@ +import { Body, Controller, Delete, Get, Logger, Param, Post, Put, Query, Request, Res } from '@nestjs/common'; +import { ApiBadRequestResponse, ApiBearerAuth, ApiForbiddenResponse, ApiOperation, ApiQuery, ApiResponse, ApiTags, ApiUnauthorizedResponse } from '@nestjs/swagger'; +import { ApiResponseDto } from '../dtos/apiResponse.dto'; +import { BadRequestErrorDto } from '../dtos/bad-request-error.dto'; +import { GenerateAuthenticationDto, GenerateRegistrationDto, UpdateFidoUserDetailsDto, VerifyRegistrationDto, VerifyAuthenticationDto } from '../dtos/fido-user.dto'; +import { ForbiddenErrorDto } from '../dtos/forbidden-error.dto'; +import { InternalServerErrorDto } from '../dtos/internal-server-error-res.dto'; +import { UnauthorizedErrorDto } from '../dtos/unauthorized-error.dto'; +import { FidoService } from './fido.service'; +import { ResponseMessages } from '@credebl/common/response-messages'; +import { HttpStatus } from '@nestjs/common'; +import IResponseType from '@credebl/common/interfaces/response.interface'; +import { Response } from 'express'; +import { Roles } from '../authz/decorators/roles.decorator'; +import { OrgRoles } from 'libs/org-roles/enums'; + +@Controller('fido') +@ApiTags('fido') +@ApiUnauthorizedResponse({ status: 401, description: 'Unauthorized', type: UnauthorizedErrorDto }) +@ApiForbiddenResponse({ status: 403, description: 'Forbidden', type: ForbiddenErrorDto }) +@ApiBadRequestResponse({ status: 400, description: 'Bad Request', type: BadRequestErrorDto }) +export class FidoController { + private logger = new Logger('FidoController'); + constructor(private readonly fidoService: FidoService) { } + /** + * + * @param GenerateRegistrationDto + * @param res + * @returns Generate registration response + */ + @Post('/generate-registration-options') + @ApiResponse({ + status: 500, + description: 'Internal server error', + type: InternalServerErrorDto + }) + @ApiOperation({ summary: 'Generate registration option' }) + @ApiResponse({ status: 201, description: 'Success', type: ApiResponseDto }) + async generateRegistrationOption(@Body() body: GenerateRegistrationDto, @Res() res: Response): Promise { + try { + const { userName, deviceFlag } = body; + const registrationOption = await this.fidoService.generateRegistrationOption(userName, deviceFlag); + + const finalResponse: IResponseType = { + statusCode: HttpStatus.OK, + message: ResponseMessages.fido.success.RegistrationOption, + data: registrationOption.response + }; + return res.status(HttpStatus.OK).json(finalResponse); + } catch (error) { + this.logger.error(`Error::${error}`); + throw error; + } + } + + /** + * + * @param VerifyRegistrationDto + * @param res + * @returns User create success + */ + @Post('/verify-registration/:userName') + @ApiResponse({ status: 201, description: 'Success', type: ApiResponseDto }) + @ApiOperation({ summary: 'Verify registration' }) + async verifyRegistration(@Request() req, @Body() verifyRegistrationDto: VerifyRegistrationDto, @Param('userName') userName: string, @Res() res: Response): Promise { + const verifyRegistration = await this.fidoService.verifyRegistration(verifyRegistrationDto, req.params.userName); + const finalResponse: IResponseType = { + statusCode: HttpStatus.OK, + message: ResponseMessages.fido.success.verifyRegistration, + data: verifyRegistration.response + }; + return res.status(HttpStatus.OK).json(finalResponse); + } + + /** + * + * @param updateFidoUserDetailsDto + * @param res + * @returns User update success + */ + @Put('/user-update') + @ApiResponse({ status: 201, description: 'Success', type: ApiResponseDto }) + @ApiOperation({ summary: 'Update fido user details' }) + async updateFidoUser(@Request() req, @Body() updateFidoUserDetailsDto: UpdateFidoUserDetailsDto, @Res() res: Response): Promise { + const verifyRegistration = await this.fidoService.updateFidoUser(updateFidoUserDetailsDto); + const finalResponse: IResponseType = { + statusCode: HttpStatus.OK, + message: ResponseMessages.fido.success.updateUserDetails, + data: verifyRegistration.response + }; + return res.status(HttpStatus.OK).json(finalResponse); + } + + /** + * + * @param GenerateAuthenticationDto + * @param res + * @returns Generate authentication object + */ + @Post('/generate-authentication-options') + @ApiOperation({ summary: 'Generate authentication option' }) + @ApiResponse({ status: 201, description: 'Success', type: ApiResponseDto }) + async generateAuthenticationOption(@Body() body: GenerateAuthenticationDto, @Request() req, @Res() res: Response): Promise { + + const generateAuthentication = await this.fidoService.generateAuthenticationOption(body); + const finalResponse: IResponseType = { + statusCode: HttpStatus.OK, + message: ResponseMessages.fido.success.generateAuthenticationOption, + data: generateAuthentication.response + }; + return res.status(HttpStatus.OK).json(finalResponse); + } + + /** + * + * @param verifyAuthenticationDto + * @param res + * @returns Verify authentication object + */ + @Post('/verify-authentication/:userName') + @ApiOperation({ summary: 'Verify authentication' }) + async verifyAuthentication(@Request() req, @Body() verifyAuthenticationDto: VerifyAuthenticationDto, @Param('userName') userName: string, @Res() res: Response): Promise { + const verifyAuthentication = await this.fidoService.verifyAuthentication(verifyAuthenticationDto, req.params.userName); + + const finalResponse: IResponseType = { + statusCode: HttpStatus.OK, + message: ResponseMessages.fido.success.login, + data: verifyAuthentication.response + }; + return res.status(HttpStatus.OK).json(finalResponse); + } + + /** + * + * @param userName + * @param res + * @returns User get success + */ + @Get('/user-details/:userName') + @Roles(OrgRoles.OWNER, OrgRoles.ADMIN, OrgRoles.HOLDER, OrgRoles.ISSUER, OrgRoles.SUPER_ADMIN, OrgRoles.SUPER_ADMIN, OrgRoles.MEMBER) + @ApiBearerAuth() + @ApiResponse({ + status: 500, + description: 'Internal server error', + type: InternalServerErrorDto + }) + + @ApiOperation({ summary: 'Fetch fido user details' }) + @ApiResponse({ status: 201, description: 'Success', type: ApiResponseDto }) + @ApiUnauthorizedResponse({ status: 401, description: 'Unauthorized', type: UnauthorizedErrorDto }) + @ApiForbiddenResponse({ status: 403, description: 'Forbidden', type: ForbiddenErrorDto }) + @ApiBadRequestResponse({ status: 400, description: 'Bad Request', type: BadRequestErrorDto }) + async fetchFidoUserDetails(@Request() req, @Param('userName') userName: string, @Res() res: Response): Promise { + try { + const fidoUserDetails = await this.fidoService.fetchFidoUserDetails(req.params.userName); + const finalResponse: IResponseType = { + statusCode: HttpStatus.OK, + message: ResponseMessages.user.success.login, + data: fidoUserDetails.response + }; + return res.status(HttpStatus.OK).json(finalResponse); + + } catch (error) { + this.logger.error(`Error::${error}`); + throw error; + } + } + + @Delete('/device') + @Roles(OrgRoles.OWNER, OrgRoles.ADMIN, OrgRoles.HOLDER, OrgRoles.ISSUER, OrgRoles.SUPER_ADMIN, OrgRoles.SUPER_ADMIN, OrgRoles.MEMBER) + @ApiBearerAuth() + @ApiResponse({ + status: 500, + description: 'Internal server error', + type: InternalServerErrorDto + }) + @ApiQuery( + { name: 'credentialId', required: true } + ) + @ApiOperation({ summary: 'Delete fido user device' }) + @ApiResponse({ status: 201, description: 'Success', type: ApiResponseDto }) + async deleteFidoUserDevice(@Query('credentialId') credentialId: string, @Res() res: Response): Promise { + try { + const deleteFidoUser = await this.fidoService.deleteFidoUserDevice(credentialId); + const finalResponse: IResponseType = { + statusCode: HttpStatus.OK, + message: ResponseMessages.fido.success.deleteDevice, + data: deleteFidoUser.response + }; + return res.status(HttpStatus.OK).json(finalResponse); + + } catch (error) { + this.logger.error(`Error::${error}`); + throw error; + } + } + + @Put('/device-name') + @Roles(OrgRoles.OWNER, OrgRoles.ADMIN, OrgRoles.HOLDER, OrgRoles.ISSUER, OrgRoles.SUPER_ADMIN, OrgRoles.SUPER_ADMIN, OrgRoles.MEMBER) + @ApiBearerAuth() + @ApiResponse({ + status: 500, + description: 'Internal server error', + type: InternalServerErrorDto + }) + @ApiQuery( + { name: 'credentialId', required: true } + ) + @ApiQuery( + { name: 'deviceName', required: true } + ) + @ApiOperation({ summary: 'Update fido user device name' }) + @ApiResponse({ status: 201, description: 'Success', type: ApiResponseDto }) + async updateFidoUserDeviceName(@Query('credentialId') credentialId: string, @Query('deviceName') deviceName: string, @Res() res: Response): Promise { + try { + const updateDeviceName = await this.fidoService.updateFidoUserDeviceName(credentialId, deviceName); + const finalResponse: IResponseType = { + statusCode: HttpStatus.OK, + message: ResponseMessages.fido.success.updateDeviceName, + data: updateDeviceName.response + }; + return res.status(HttpStatus.OK).json(finalResponse); + } catch (error) { + this.logger.error(`Error::${error}`); + throw error; + } + } + +} diff --git a/apps/api-gateway/src/fido/fido.module.ts b/apps/api-gateway/src/fido/fido.module.ts new file mode 100644 index 000000000..1011c9b95 --- /dev/null +++ b/apps/api-gateway/src/fido/fido.module.ts @@ -0,0 +1,21 @@ +import { Module } from '@nestjs/common'; +import { ClientsModule, Transport } from '@nestjs/microservices'; +import { FidoController } from './fido.controller'; +import { FidoService } from './fido.service'; + +@Module({ + imports:[ + ClientsModule.register([ + { + name: 'NATS_CLIENT', + transport: Transport.NATS, + options: { + servers: [`${process.env.NATS_URL}`] + } + } + ]) + ], + controllers: [FidoController], + providers: [FidoService] +}) +export class FidoModule { } diff --git a/apps/api-gateway/src/fido/fido.service.ts b/apps/api-gateway/src/fido/fido.service.ts new file mode 100644 index 000000000..cb9e87ad0 --- /dev/null +++ b/apps/api-gateway/src/fido/fido.service.ts @@ -0,0 +1,58 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { ClientProxy, RpcException } from '@nestjs/microservices'; +import { BaseService } from 'libs/service/base.service'; +import { UpdateFidoUserDetailsDto, VerifyRegistrationDto, GenerateAuthenticationDto, VerifyAuthenticationDto } from '../dtos/fido-user.dto'; + + +@Injectable() +export class FidoService extends BaseService { + constructor( + @Inject('NATS_CLIENT') private readonly fidoServiceProxy: ClientProxy + ) { + super('FidoService'); + } + async generateRegistrationOption(userName: string, deviceFlag: boolean): Promise<{response: object}> { + try { + const payload = { userName, deviceFlag }; + return this.sendNats(this.fidoServiceProxy, 'generate-registration-options', payload); + } catch (error) { + throw new RpcException(error.response); + } + + } + + async verifyRegistration(verifyRegistrationDto: VerifyRegistrationDto, userName: string): Promise<{response: object}> { + const payload = { verifyRegistrationDetails: verifyRegistrationDto, userName }; + return this.sendNats(this.fidoServiceProxy, 'verify-registration', payload); + } + + async generateAuthenticationOption(generateAuthentication: GenerateAuthenticationDto) : Promise<{response: object}> { + const payload = { generateAuthentication }; + return this.sendNats(this.fidoServiceProxy, 'generate-authentication-options', payload); + } + + async verifyAuthentication(verifyAuthenticationDto: VerifyAuthenticationDto, userName: string): Promise<{response: object}> { + const payload = { verifyAuthenticationDetails: verifyAuthenticationDto, userName }; + return this.sendNats(this.fidoServiceProxy, 'verify-authentication', payload); + } + + async updateFidoUser(updateFidoUserDetailsDto: UpdateFidoUserDetailsDto) : Promise<{response: object}> { + const payload = updateFidoUserDetailsDto; + return this.sendNats(this.fidoServiceProxy, 'update-user', payload); + } + + async fetchFidoUserDetails(userName: string): Promise<{response: string}> { + const payload = { userName }; + return this.sendNats(this.fidoServiceProxy, 'fetch-fido-user-details', payload); + } + + async deleteFidoUserDevice(credentialId: string): Promise<{response: object}> { + const payload = { credentialId }; + return this.sendNats(this.fidoServiceProxy, 'delete-fido-user-device', payload); + } + + async updateFidoUserDeviceName(credentialId: string, deviceName: string): Promise<{response: string}> { + const payload = { credentialId, deviceName }; + return this.sendNats(this.fidoServiceProxy, 'update-fido-user-device-name', payload); + } +} diff --git a/apps/api-gateway/src/interfaces/ISchemaSearch.interface.ts b/apps/api-gateway/src/interfaces/ISchemaSearch.interface.ts new file mode 100644 index 000000000..3979fe62c --- /dev/null +++ b/apps/api-gateway/src/interfaces/ISchemaSearch.interface.ts @@ -0,0 +1,18 @@ +import { IUserRequestInterface } from '../schema/interfaces'; + +export interface ISchemaSearchInterface { + pageNumber: number; + pageSize: number; + sorting: string; + sortByValue: string; + searchByText: string; + user?: IUserRequestInterface +} + +export interface ICredDeffSchemaSearchInterface { + pageNumber: number; + pageSize: number; + sorting: string; + sortByValue: string; + user?: IUserRequestInterface +} \ No newline at end of file diff --git a/apps/api-gateway/src/interfaces/ISocket.interface.ts b/apps/api-gateway/src/interfaces/ISocket.interface.ts new file mode 100644 index 000000000..d2debdc11 --- /dev/null +++ b/apps/api-gateway/src/interfaces/ISocket.interface.ts @@ -0,0 +1,9 @@ +export interface ISocketInterface { + token?: string; + message?: string; + clientSocketId?: string; + clientId?: string; + error?: string; + connectionId?: string; + demoFlow?: string; +} \ No newline at end of file diff --git a/apps/api-gateway/src/interfaces/IUserRequestInterface.ts b/apps/api-gateway/src/interfaces/IUserRequestInterface.ts new file mode 100644 index 000000000..f5bc9a3bc --- /dev/null +++ b/apps/api-gateway/src/interfaces/IUserRequestInterface.ts @@ -0,0 +1,15 @@ +import { UserRoleOrgPermsDto } from '../authz/dtos/user-role-org-perms.dto'; + +export interface IUserRequestInterface { + userId: number; + email: string; + orgId: number; + agentEndPoint?: string; + apiKey?: string; + tenantId?: number; + tenantName?: string; + tenantOrgId?: number; + userRoleOrgPermissions?: UserRoleOrgPermsDto[]; + orgName?:string +} + diff --git a/apps/api-gateway/src/interfaces/fileExport.interface.ts b/apps/api-gateway/src/interfaces/fileExport.interface.ts new file mode 100644 index 000000000..4cc78a3c3 --- /dev/null +++ b/apps/api-gateway/src/interfaces/fileExport.interface.ts @@ -0,0 +1,13 @@ +import { IUserRequestInterface } from './IUserRequestInterface'; + +export interface FileExportResponse { + fileContent: string; + fileName : string +} + +export interface FileImportRequest { + filePath: string; + fileName : string; + credDefId: string; + user : IUserRequestInterface +} \ No newline at end of file diff --git a/apps/api-gateway/src/issuance/dtos/issuance.dto.ts b/apps/api-gateway/src/issuance/dtos/issuance.dto.ts new file mode 100644 index 000000000..d23629f93 --- /dev/null +++ b/apps/api-gateway/src/issuance/dtos/issuance.dto.ts @@ -0,0 +1,98 @@ +import { IsArray, IsNotEmpty, IsNumber, IsOptional, IsString } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +interface attribute { + name: string; + value: string; +} + +export class IssueCredentialDto { + + @ApiProperty({ example: [{ 'value': 'string', 'name': 'string' }] }) + @IsNotEmpty({ message: 'Please provide valid attributes' }) + @IsArray({ message: 'attributes should be array' }) + attributes: attribute[]; + + @ApiProperty({ example: 'string' }) + @IsNotEmpty({ message: 'Please provide valid credentialDefinitionId' }) + @IsString({ message: 'credentialDefinitionId should be string' }) + credentialDefinitionId: string; + + @ApiProperty({ example: 'string' }) + @IsNotEmpty({ message: 'Please provide valid comment' }) + @IsString({ message: 'comment should be string' }) + comment: string; + + @ApiProperty({ example: '3fa85f64-5717-4562-b3fc-2c963f66afa6' }) + @IsNotEmpty({ message: 'Please provide valid connectionId' }) + @IsString({ message: 'connectionId should be string' }) + connectionId: string; + + @ApiProperty() + @IsNumber() + @IsNotEmpty({ message: 'please provide orgId' }) + orgId: number; +} + + +export class IssuanceDto { + @ApiProperty() + @IsOptional() + _tags?: object; + + @ApiProperty() + @IsOptional() + metadata?: object; + + @ApiProperty() + @IsOptional() + credentials: object[]; + + @ApiProperty() + @IsOptional() + id: string; + + @ApiProperty() + @IsOptional() + createdAt: string; + + @ApiProperty() + @IsOptional() + state: string; + + @ApiProperty() + @IsOptional() + connectionId: string; + + @ApiProperty() + @IsOptional() + protocolVersion: string; + + @ApiProperty() + @IsOptional() + threadId: string; + + @ApiProperty() + @IsOptional() + credentialAttributes: CredentialAttributes[]; + + @ApiProperty() + @IsOptional() + autoAcceptCredential: string; +} + + +export class CredentialAttributes { + @ApiProperty() + @IsOptional() + 'mime-type'?: string; + + @ApiProperty() + @IsOptional() + name?: string; + + @ApiProperty() + @IsOptional() + value: string; +} + diff --git a/apps/api-gateway/src/issuance/enums/Issuance.enum.ts b/apps/api-gateway/src/issuance/enums/Issuance.enum.ts new file mode 100644 index 000000000..708bf8e8e --- /dev/null +++ b/apps/api-gateway/src/issuance/enums/Issuance.enum.ts @@ -0,0 +1,13 @@ +export enum IssueCredential { + proposalSent = 'proposal-sent', + proposalReceived = 'proposal-received', + offerSent = 'offer-sent', + offerReceived = 'offer-received', + declined = 'decliend', + requestSent = 'request-sent', + requestReceived = 'request-received', + credentialIssued = 'credential-issued', + credentialReceived = 'credential-received', + done = 'done', + abandoned = 'abandoned' +} \ 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 new file mode 100644 index 000000000..46ff1044a --- /dev/null +++ b/apps/api-gateway/src/issuance/interfaces/index.ts @@ -0,0 +1,57 @@ +export interface IUserRequestInterface { + userId: number; + email: string; + orgId: number; + agentEndPoint?: string; + apiKey?: string; + tenantId?: number; + tenantName?: string; + tenantOrgId?: number; + userRoleOrgPermissions?: IUserRoleOrgPerms[]; + orgName?: string; + selectedOrg: ISelectedOrg; +} + +export interface ISelectedOrg { + id: number; + userId: number; + orgRoleId: number; + orgId: number; + orgRole: object; + organisation: object; +} + +export interface IOrganization { + name: string; + description: string; + org_agents: IOrgAgent[] + +} + +export interface IOrgAgent { + orgDid: string; + verkey: string; + agentEndPoint: string; + agentOptions: string; + walletName: string; + agentsTypeId: string; + orgId: string; +} + +export class IUserRoleOrgPerms { + id: number; + role: IUserRole; + Organization: IUserOrg; +} + +export class IUserRole { + id: number; + name: string; + permissions: string[]; + +} + +export class IUserOrg { + id: number; + orgName: 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 new file mode 100644 index 000000000..676586072 --- /dev/null +++ b/apps/api-gateway/src/issuance/issuance.controller.ts @@ -0,0 +1,207 @@ +/* eslint-disable no-param-reassign */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable camelcase */ +import { + Controller, + Post, + Body, + Logger, + UseGuards, + BadRequestException, + HttpStatus, + Res, + Query, + Get, + Param +} from '@nestjs/common'; +import { + ApiTags, + ApiOperation, + ApiResponse, + ApiBearerAuth, + ApiForbiddenResponse, + ApiUnauthorizedResponse, + ApiQuery, + ApiExcludeEndpoint +} from '@nestjs/swagger'; +import { AuthGuard } from '@nestjs/passport'; +import { ApiResponseDto } from '../dtos/apiResponse.dto'; +import { UnauthorizedErrorDto } from '../dtos/unauthorized-error.dto'; +import { ForbiddenErrorDto } from '../dtos/forbidden-error.dto'; +import { CommonService } from '@credebl/common/common.service'; +import { Response } from 'express'; +import IResponseType from '@credebl/common/interfaces/response.interface'; +import { IssuanceService } from './issuance.service'; +import { IssuanceDto, IssueCredentialDto } from './dtos/issuance.dto'; +import { IUserRequest } from '@credebl/user-request/user-request.interface'; +import { User } from '../authz/decorators/user.decorator'; +import { ResponseMessages } from '@credebl/common/response-messages'; +import { IssueCredential } from './enums/Issuance.enum'; + +@Controller() +@ApiTags('issuances') +@ApiUnauthorizedResponse({ status: 401, description: 'Unauthorized', type: UnauthorizedErrorDto }) +@ApiForbiddenResponse({ status: 403, description: 'Forbidden', type: ForbiddenErrorDto }) + +export class IssuanceController { + constructor( + private readonly issueCredentialService: IssuanceService, + private readonly commonService: CommonService + + ) { } + private readonly logger = new Logger('IssuanceController'); + + /** + * Description: Issuer send credential to create offer + * @param user + * @param issueCredentialDto + */ + @Post('issue-credentials/create-offer') + @UseGuards(AuthGuard('jwt')) + @ApiBearerAuth() + @ApiOperation({ + summary: `Send credential details to create-offer`, + description: `Send credential details to create-offer` + }) + @ApiResponse({ status: 201, description: 'Success', type: ApiResponseDto }) + async sendCredential( + @User() user: IUserRequest, + @Body() issueCredentialDto: IssueCredentialDto, + @Res() res: Response + ): Promise { + + const attrData = issueCredentialDto.attributes; + + attrData.forEach((data) => { + if ('' === data['name'].trim()) { + throw new BadRequestException(`Name must be required`); + } else if ('' === data['value'].trim()) { + throw new BadRequestException(`Value must be required at position of ${data['name']}`); + } + }); + const getCredentialDetails = await this.issueCredentialService.sendCredentialCreateOffer( + issueCredentialDto, + user + ); + + const finalResponse: IResponseType = { + statusCode: HttpStatus.CREATED, + message: ResponseMessages.issuance.success.create, + data: getCredentialDetails.response + }; + return res.status(HttpStatus.CREATED).json(finalResponse); + } + + /** + * Description: webhook Save issued credential details + * @param user + * @param issueCredentialDto + */ + @Post('wh/:id/credentials') + @ApiExcludeEndpoint() + @ApiOperation({ + summary: 'Catch issue credential webhook responses', + description: 'Callback URL for issue credential' + }) + async getIssueCredentialWebhook( + @Body() issueCredentialDto: IssuanceDto, + @Param('id') id: number, + @Res() res: Response + ): Promise { + const getCredentialDetails = await this.issueCredentialService.getIssueCredentialWebhook(issueCredentialDto, id); + const finalResponse: IResponseType = { + statusCode: HttpStatus.CREATED, + message: ResponseMessages.issuance.success.create, + data: getCredentialDetails.response + }; + return res.status(HttpStatus.CREATED).json(finalResponse); + + } + + /** + * Description: Get all issued credentials + * @param user + * @param threadId + * @param connectionId + * @param state + * @param orgId + * + */ + @Get('/issue-credentials') + @UseGuards(AuthGuard('jwt')) + @ApiBearerAuth() + @ApiOperation({ + summary: `Fetch all issued credentials`, + description: `Fetch all issued credentials` + }) + @ApiResponse({ status: 201, description: 'Success', type: ApiResponseDto }) + @ApiQuery( + { name: 'threadId', required: false } + ) + @ApiQuery( + { name: 'connectionId', required: false } + ) + @ApiQuery( + { name: 'state', enum: IssueCredential, required: false } + ) + @ApiQuery( + { name: 'orgId', required: true } + ) + async getIssueCredentials( + @User() user: IUserRequest, + @Query('threadId') threadId: string, + @Query('connectionId') connectionId: string, + @Query('state') state: string, + @Query('orgId') orgId: number, + @Res() res: Response + ): Promise { + + state = state || undefined; + const getCredentialDetails = await this.issueCredentialService.getIssueCredentials(user, threadId, connectionId, state, orgId); + + const finalResponse: IResponseType = { + statusCode: HttpStatus.CREATED, + message: ResponseMessages.issuance.success.fetch, + data: getCredentialDetails.response + }; + return res.status(HttpStatus.CREATED).json(finalResponse); + } + + + /** + * Description: Get all issued credentials + * @param user + * @param credentialRecordId + * @param orgId + * + */ + @Get('issue-credentials/:credentialRecordId') + @UseGuards(AuthGuard('jwt')) + @ApiBearerAuth() + @ApiOperation({ + summary: `Fetch all issued credentials by credentialRecordId`, + description: `Fetch all issued credentials by credentialRecordId` + }) + @ApiQuery( + { name: 'orgId', required: true } + ) + @ApiResponse({ status: 201, description: 'Success', type: ApiResponseDto }) + async getIssueCredentialsbyCredentialRecordId( + @User() user: IUserRequest, + @Param('credentialRecordId') credentialRecordId: string, + @Query('orgId') orgId: number, + + @Res() res: Response + ): Promise { + + const getCredentialDetails = await this.issueCredentialService.getIssueCredentialsbyCredentialRecordId(user, credentialRecordId, orgId); + + const finalResponse: IResponseType = { + statusCode: HttpStatus.CREATED, + message: ResponseMessages.issuance.success.fetch, + data: getCredentialDetails.response + }; + return res.status(HttpStatus.CREATED).json(finalResponse); + } + +} diff --git a/apps/api-gateway/src/issuance/issuance.module.ts b/apps/api-gateway/src/issuance/issuance.module.ts new file mode 100644 index 000000000..5e339bd5b --- /dev/null +++ b/apps/api-gateway/src/issuance/issuance.module.ts @@ -0,0 +1,24 @@ +import { ClientsModule, Transport } from '@nestjs/microservices'; +import { Module } from '@nestjs/common'; +import { IssuanceController } from './issuance.controller'; +import { IssuanceService } from './issuance.service'; +import { CommonService } from '@credebl/common'; +import { HttpModule } from '@nestjs/axios'; + +@Module({ + imports: [ + HttpModule, + ClientsModule.register([ + { + name: 'NATS_CLIENT', + transport: Transport.NATS, + options: { + servers: [`${process.env.NATS_URL}`] + } + } + ]) + ], + controllers: [IssuanceController], + providers: [IssuanceService, CommonService] +}) +export class IssuanceModule { } diff --git a/apps/api-gateway/src/issuance/issuance.service.ts b/apps/api-gateway/src/issuance/issuance.service.ts new file mode 100644 index 000000000..613064ec5 --- /dev/null +++ b/apps/api-gateway/src/issuance/issuance.service.ts @@ -0,0 +1,53 @@ +import { Injectable, Inject } from '@nestjs/common'; +import { ClientProxy } from '@nestjs/microservices'; +import { BaseService } from 'libs/service/base.service'; +import { IUserRequest } from '@credebl/user-request/user-request.interface'; +import { IssuanceDto, IssueCredentialDto } from './dtos/issuance.dto'; + +@Injectable() +export class IssuanceService extends BaseService { + + + constructor( + @Inject('NATS_CLIENT') private readonly issuanceProxy: ClientProxy + ) { + super('IssuanceService'); + } + + sendCredentialCreateOffer(issueCredentialDto: IssueCredentialDto, user: IUserRequest): Promise<{ + response: object; + }> { + const payload = { attributes: issueCredentialDto.attributes, comment: issueCredentialDto.comment, credentialDefinitionId: issueCredentialDto.credentialDefinitionId, connectionId: issueCredentialDto.connectionId, orgId: issueCredentialDto.orgId, user }; + return this.sendNats(this.issuanceProxy, 'send-credential-create-offer', payload); + } + + sendCredentialOutOfBand(issueCredentialDto: IssueCredentialDto, user: IUserRequest): Promise<{ + response: object; + }> { + const payload = { attributes: issueCredentialDto.attributes, comment: issueCredentialDto.comment, credentialDefinitionId: issueCredentialDto.credentialDefinitionId, connectionId: issueCredentialDto.connectionId, orgId: issueCredentialDto.orgId, user }; + return this.sendNats(this.issuanceProxy, 'send-credential-create-offer-oob', payload); + } + + + getIssueCredentials(user: IUserRequest, threadId: string, connectionId: string, state: string, orgId: number): Promise<{ + response: object; + }> { + const payload = { user, threadId, connectionId, state, orgId }; + return this.sendNats(this.issuanceProxy, 'get-all-issued-credentials', payload); + } + + getIssueCredentialsbyCredentialRecordId(user: IUserRequest, credentialRecordId: string, orgId: number): Promise<{ + response: object; + }> { + const payload = { user, credentialRecordId, orgId }; + return this.sendNats(this.issuanceProxy, 'get-issued-credentials-by-credentialDefinitionId', payload); + } + + getIssueCredentialWebhook(issueCredentialDto: IssuanceDto, id: number): Promise<{ + response: object; + }> { + const payload = { createDateTime: issueCredentialDto.createdAt, connectionId: issueCredentialDto.connectionId, threadId: issueCredentialDto.threadId, protocolVersion: issueCredentialDto.protocolVersion, credentialAttributes: issueCredentialDto.credentialAttributes, orgId: id }; + return this.sendNats(this.issuanceProxy, 'webhook-get-issue-credential', payload); + } + +} diff --git a/apps/api-gateway/src/main.ts b/apps/api-gateway/src/main.ts new file mode 100644 index 000000000..828a21b51 --- /dev/null +++ b/apps/api-gateway/src/main.ts @@ -0,0 +1,73 @@ +import * as dotenv from 'dotenv'; +import * as express from 'express'; + +import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; +import { Logger, ValidationPipe } from '@nestjs/common'; + +import { AppModule } from './app.module'; +import { HttpAdapterHost, NestFactory } from '@nestjs/core'; +import { AllExceptionsFilter } from '@credebl/common/exception-handler'; + +// const fs = require('fs'); + + +dotenv.config(); + +// async function readSecretFile(filename: string): Promise { +// return fs.readFile(filename, 'utf8', function (err, data) { +// // Display the file content +// return data; +// }); +// } + +async function bootstrap(): Promise { + + // const httpsOptions = { + // key: await readSecretFile(''), + // cert: await readSecretFile(''), + // }; + + // const config = new ConfigService(); + const app = await NestFactory.create(AppModule, { + // httpsOptions, + }); + app.use(express.json({ limit: '50mb' })); + app.use(express.urlencoded({ limit: '50mb' })); + + app.useGlobalPipes(new ValidationPipe()); + const options = new DocumentBuilder() + .setTitle(`${process.env.PLATFORM_NAME}`) + .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.API_GATEWAY_PROTOCOL}://${process.env.API_ENDPOINT}`) + .addServer(`${process.env.API_GATEWAY_PROTOCOL}://${process.env.API_GATEWAY_HOST}`) + .build(); + + const document = SwaggerModule.createDocument(app, options); + SwaggerModule.setup('api', app, document); + const httpAdapter = app.get(HttpAdapterHost); + app.useGlobalFilters(new AllExceptionsFilter(httpAdapter)); + app.enableCors(); + + app.use(express.static('uploadedFiles/holder-profile')); + app.use(express.static('uploadedFiles/org-logo')); + app.use(express.static('uploadedFiles/tenant-logo')); + app.use(express.static('uploadedFiles/exports')); + app.use(express.static('resources')); + app.use(express.static('genesis-file')); + app.use(express.static('invoice-pdf')); + app.use(express.static('uploadedFiles/bulk-verification-templates')); + app.use(express.static('uploadedFiles/exports')); + app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true })); + + await app.listen(process.env.API_GATEWAY_PORT, `${process.env.API_GATEWAY_HOST}`); + Logger.log(`API Gateway is listening on port ${process.env.API_GATEWAY_PORT}`); +} +bootstrap(); + diff --git a/apps/api-gateway/src/organization/dtos/create-organization-dto.ts b/apps/api-gateway/src/organization/dtos/create-organization-dto.ts new file mode 100644 index 000000000..e71779445 --- /dev/null +++ b/apps/api-gateway/src/organization/dtos/create-organization-dto.ts @@ -0,0 +1,36 @@ +import { ApiExtraModels, ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IsNotEmpty, IsOptional, IsString, MaxLength, MinLength } from 'class-validator'; + +import { Transform } from 'class-transformer'; +import { trim } from '@credebl/common/cast.helper'; + +@ApiExtraModels() +export class CreateOrganizationDto { + + @ApiProperty() + @Transform(({ value }) => trim(value)) + @IsNotEmpty({ message: 'Organization name is required.' }) + @MinLength(2, { message: 'Organization name must be at least 2 characters.' }) + @MaxLength(50, { message: 'Organization name must be at most 50 characters.' }) + @IsString({ message: 'Organization name must be in string format.' }) + name: string; + + @ApiPropertyOptional() + @Transform(({ value }) => trim(value)) + @MinLength(2, { message: 'Description must be at least 2 characters.' }) + @MaxLength(255, { message: 'Description must be at most 255 characters.' }) + @IsString({ message: 'Description must be in string format.' }) + description: string; + + @ApiPropertyOptional() + @IsOptional() + @Transform(({ value }) => trim(value)) + @IsString({ message: 'logo must be in string format.' }) + logo: string; + + @ApiPropertyOptional() + @IsOptional() + @Transform(({ value }) => trim(value)) + website?: string; + +} \ No newline at end of file diff --git a/apps/api-gateway/src/organization/dtos/get-all-organizations.dto.ts b/apps/api-gateway/src/organization/dtos/get-all-organizations.dto.ts new file mode 100644 index 000000000..53d0a082f --- /dev/null +++ b/apps/api-gateway/src/organization/dtos/get-all-organizations.dto.ts @@ -0,0 +1,27 @@ +import { Transform, Type } from 'class-transformer'; +// import { SortValue } from '../../enum'; +import { toNumber, trim } from '@credebl/common/cast.helper'; + +import { ApiProperty } from '@nestjs/swagger'; +import { IsOptional } from 'class-validator'; + +export class GetAllOrganizationsDto { + @ApiProperty({ required: false }) + @IsOptional() + @Type(() => Number) + @Transform(({ value }) => toNumber(value)) + pageNumber = 1; + + @ApiProperty({ required: false }) + @IsOptional() + @Type(() => String) + @Transform(({ value }) => trim(value)) + search = ''; + + @ApiProperty({ required: false }) + @IsOptional() + @Type(() => Number) + @Transform(({ value }) => toNumber(value)) + pageSize = 10; + +} diff --git a/apps/api-gateway/src/organization/dtos/get-all-sent-invitations.dto.ts b/apps/api-gateway/src/organization/dtos/get-all-sent-invitations.dto.ts new file mode 100644 index 000000000..edc9eaf1c --- /dev/null +++ b/apps/api-gateway/src/organization/dtos/get-all-sent-invitations.dto.ts @@ -0,0 +1,26 @@ +import { Transform, Type } from 'class-transformer'; +import { toNumber, trim } from '@credebl/common/cast.helper'; + +import { ApiProperty } from '@nestjs/swagger'; +import { IsOptional } from 'class-validator'; + +export class GetAllSentInvitationsDto { + @ApiProperty({ required: false }) + @IsOptional() + @Type(() => Number) + @Transform(({ value }) => toNumber(value)) + pageNumber = 1; + + @ApiProperty({ required: false }) + @IsOptional() + @Type(() => String) + @Transform(({ value }) => trim(value)) + search = ''; + + @ApiProperty({ required: false }) + @IsOptional() + @Type(() => Number) + @Transform(({ value }) => toNumber(value)) + pageSize = 10; + +} diff --git a/apps/api-gateway/src/organization/dtos/send-invitation.dto.ts b/apps/api-gateway/src/organization/dtos/send-invitation.dto.ts new file mode 100644 index 000000000..a6c1b43c8 --- /dev/null +++ b/apps/api-gateway/src/organization/dtos/send-invitation.dto.ts @@ -0,0 +1,37 @@ +import { ApiExtraModels, ApiProperty } from '@nestjs/swagger'; +import { IsArray, IsEmail, IsNotEmpty, IsNumber, IsString, ValidateNested } from 'class-validator'; +import { Transform, Type } from 'class-transformer'; + +import { trim } from '@credebl/common/cast.helper'; + +@ApiExtraModels() +export class SendInvitationDto { + + @ApiProperty({ example: 'awqx@getnada.com' }) + @IsEmail() + @Transform(({ value }) => trim(value)) + @IsNotEmpty({ message: 'Please provide valid email' }) + @IsString({ message: 'email should be string' }) + email: string; + + @ApiProperty({ example: [2, 1, 3] }) + @IsNotEmpty({ message: 'Please provide valid orgRoleId' }) + @IsArray() + orgRoleId: number[]; + +} + +@ApiExtraModels() +export class BulkSendInvitationDto { + + @ApiProperty({ type: [SendInvitationDto] }) + @IsArray() + @ValidateNested({ each: true }) + @Type(() => SendInvitationDto) + invitations: SendInvitationDto[]; + + @ApiProperty({ example: 1 }) + @IsNotEmpty({ message: 'Please provide valid orgId' }) + @IsNumber() + orgId: number; +} \ No newline at end of file diff --git a/apps/api-gateway/src/organization/dtos/update-organization-dto.ts b/apps/api-gateway/src/organization/dtos/update-organization-dto.ts new file mode 100644 index 000000000..44569b38d --- /dev/null +++ b/apps/api-gateway/src/organization/dtos/update-organization-dto.ts @@ -0,0 +1,40 @@ +import { ApiExtraModels, ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IsNotEmpty, IsNumber, IsOptional, IsString, MaxLength, MinLength } from 'class-validator'; + +import { Transform } from 'class-transformer'; +import { trim } from '@credebl/common/cast.helper'; + +@ApiExtraModels() +export class UpdateOrganizationDto { + + @ApiProperty() + @IsNotEmpty({ message: 'orgId is required.' }) + @IsNumber() + orgId: number; + + @ApiProperty() + @Transform(({ value }) => trim(value)) + @IsNotEmpty({ message: 'Organization name is required.' }) + @MinLength(2, { message: 'Organization name must be at least 2 characters.' }) + @MaxLength(50, { message: 'Organization name must be at most 50 characters.' }) + @IsString({ message: 'Organization name must be in string format.' }) + name: string; + + @ApiPropertyOptional() + @Transform(({ value }) => trim(value)) + @MinLength(2, { message: 'Description must be at least 2 characters.' }) + @MaxLength(255, { message: 'Description must be at most 255 characters.' }) + @IsString({ message: 'Description must be in string format.' }) + description: string; + + @ApiProperty() + @IsOptional() + @Transform(({ value }) => trim(value)) + @IsString({ message: 'logo must be in string format.' }) + logo: string; + + @ApiProperty() + @IsOptional() + website: string; + +} \ No newline at end of file diff --git a/apps/api-gateway/src/organization/dtos/update-user-roles.dto.ts b/apps/api-gateway/src/organization/dtos/update-user-roles.dto.ts new file mode 100644 index 000000000..5ddb6a9f8 --- /dev/null +++ b/apps/api-gateway/src/organization/dtos/update-user-roles.dto.ts @@ -0,0 +1,26 @@ +import { IsArray, IsNotEmpty, IsNumber } from 'class-validator'; + +import { ApiProperty } from '@nestjs/swagger'; +import { Transform } from 'class-transformer'; +import { toNumber } from '@credebl/common/cast.helper'; + +export class UpdateUserRolesDto { + + @ApiProperty({ example: '2' }) + @IsNotEmpty({ message: 'Please provide valid orgId' }) + @Transform(({ value }) => toNumber(value)) + @IsNumber() + orgId: number; + + @ApiProperty({ example: '3' }) + @IsNotEmpty({ message: 'Please provide valid userId' }) + @Transform(({ value }) => toNumber(value)) + @IsNumber() + userId: number; + + @ApiProperty({ example: [2, 1, 3] }) + @IsNotEmpty({ message: 'Please provide valid orgRoleId' }) + @IsArray() + orgRoleId: number[]; + +} \ No newline at end of file diff --git a/apps/api-gateway/src/organization/organization.controller.ts b/apps/api-gateway/src/organization/organization.controller.ts new file mode 100644 index 000000000..a23e5c69a --- /dev/null +++ b/apps/api-gateway/src/organization/organization.controller.ts @@ -0,0 +1,209 @@ +import { ApiBearerAuth, ApiForbiddenResponse, ApiOperation, ApiQuery, ApiResponse, ApiTags, ApiUnauthorizedResponse } from '@nestjs/swagger'; +import { CommonService } from '@credebl/common'; +import { Controller, Get, Put, Param, UseGuards } from '@nestjs/common'; +import { OrganizationService } from './organization.service'; +import { Post } from '@nestjs/common'; +import { Body } from '@nestjs/common'; +import { Res } from '@nestjs/common'; +import { CreateOrganizationDto } from './dtos/create-organization-dto'; +import IResponseType from '@credebl/common/interfaces/response.interface'; +import { HttpStatus } from '@nestjs/common'; +import { Response } from 'express'; +import { ApiResponseDto } from '../dtos/apiResponse.dto'; +import { UnauthorizedErrorDto } from '../dtos/unauthorized-error.dto'; +import { ForbiddenErrorDto } from '../dtos/forbidden-error.dto'; +import { AuthGuard } from '@nestjs/passport'; +import { User } from '../authz/decorators/user.decorator'; +import { user } from '@prisma/client'; +import { ResponseMessages } from '@credebl/common/response-messages'; +import { BulkSendInvitationDto } from './dtos/send-invitation.dto'; +import { OrgRolesGuard } from '../authz/guards/org-roles.guard'; +import { Roles } from '../authz/decorators/roles.decorator'; +import { OrgRoles } from 'libs/org-roles/enums'; +import { UpdateUserRolesDto } from './dtos/update-user-roles.dto'; +import { Query } from '@nestjs/common'; +import { GetAllOrganizationsDto } from './dtos/get-all-organizations.dto'; +import { GetAllSentInvitationsDto } from './dtos/get-all-sent-invitations.dto'; +import { UpdateOrganizationDto } from './dtos/update-organization-dto'; + +@Controller('organization') +@ApiTags('organizations') +@ApiUnauthorizedResponse({ status: 401, description: 'Unauthorized', type: UnauthorizedErrorDto }) +@ApiForbiddenResponse({ status: 403, description: 'Forbidden', type: ForbiddenErrorDto }) +export class OrganizationController { + + constructor( + private readonly organizationService: OrganizationService, + private readonly commonService: CommonService + ) { } + + @Post('/') + @ApiOperation({ summary: 'Create a new Organization', description: 'Create an organization' }) + @ApiResponse({ status: 201, description: 'Success', type: ApiResponseDto }) + @UseGuards(AuthGuard('jwt')) + @ApiBearerAuth() + async createOrganization(@Body() createOrgDto: CreateOrganizationDto, @Res() res: Response, @User() reqUser: user): Promise { + await this.organizationService.createOrganization(createOrgDto, reqUser.id); + + const finalResponse: IResponseType = { + statusCode: HttpStatus.CREATED, + message: ResponseMessages.organisation.success.create + }; + return res.status(HttpStatus.CREATED).json(finalResponse); + } + + @Get('/') + @ApiOperation({ summary: 'Get all organizations', description: 'Get all organizations' }) + @ApiResponse({ status: 200, description: 'Success', type: ApiResponseDto }) + @UseGuards(AuthGuard('jwt')) + @ApiBearerAuth() + async getOrganizations(@Query() getAllOrgsDto: GetAllOrganizationsDto, @Res() res: Response, @User() reqUser: user): Promise { + + const getOrganizations = await this.organizationService.getOrganizations(getAllOrgsDto, reqUser.id); + + const finalResponse: IResponseType = { + statusCode: HttpStatus.OK, + message: ResponseMessages.organisation.success.getOrganizations, + data: getOrganizations.response + }; + return res.status(HttpStatus.OK).json(finalResponse); + + } + + @Get('/roles') + @ApiOperation({ + summary: 'Fetch org-roles details', + description: 'Fetch org-roles details' + }) + @ApiResponse({ status: 200, description: 'Success', type: ApiResponseDto }) + @UseGuards(AuthGuard('jwt')) + @ApiBearerAuth() + async getOrgRoles(@Res() res: Response): Promise { + + const orgRoles = await this.organizationService.getOrgRoles(); + + const finalResponse: IResponseType = { + statusCode: HttpStatus.OK, + message: ResponseMessages.organisation.success.fetchOrgRoles, + data: orgRoles + }; + + return res.status(HttpStatus.OK).json(finalResponse); + + } + + @Post('/invitations') + @ApiOperation({ + summary: 'Create organization invitation', + description: 'Create send invitation' + }) + @ApiResponse({ status: 200, description: 'Success', type: ApiResponseDto }) + @Roles(OrgRoles.OWNER, OrgRoles.SUPER_ADMIN, OrgRoles.ADMIN) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + @ApiBearerAuth() + async createInvitation(@Body() bulkInvitationDto: BulkSendInvitationDto, @User() user: user, @Res() res: Response): Promise { + await this.organizationService.createInvitation(bulkInvitationDto, user.id); + + const finalResponse: IResponseType = { + statusCode: HttpStatus.CREATED, + message: ResponseMessages.organisation.success.createInvitation + }; + + return res.status(HttpStatus.CREATED).json(finalResponse); + + } + + @Get('/invitations/:id') + @ApiOperation({ summary: 'Get an invitations', description: 'Get an invitations' }) + @ApiResponse({ status: 200, description: 'Success', type: ApiResponseDto }) + @UseGuards(AuthGuard('jwt')) + @ApiBearerAuth() + async getInvitationsByOrgId(@Param('id') orgId: number, @Query() getAllInvitationsDto: GetAllSentInvitationsDto, @Res() res: Response): Promise { + + const getInvitationById = await this.organizationService.getInvitationsByOrgId(orgId, getAllInvitationsDto); + + const finalResponse: IResponseType = { + statusCode: HttpStatus.OK, + message: ResponseMessages.organisation.success.getInvitation, + data: getInvitationById.response + }; + return res.status(HttpStatus.OK).json(finalResponse); + + } + + @Get('/dashboard') + @ApiOperation({ summary: 'Get an organization', description: 'Get an organization' }) + @ApiResponse({ status: 200, description: 'Success', type: ApiResponseDto }) + @UseGuards(AuthGuard('jwt')) + @ApiBearerAuth() + @ApiQuery( + { name: 'orgId', required: true } + ) + async getOrganizationDashboard(@Query('orgId') orgId: number, @Res() res: Response, @User() reqUser: user): Promise { + + const getOrganization = await this.organizationService.getOrganizationDashboard(orgId, reqUser.id); + + const finalResponse: IResponseType = { + statusCode: HttpStatus.OK, + message: ResponseMessages.organisation.success.getOrgDashboard, + data: getOrganization.response + }; + return res.status(HttpStatus.OK).json(finalResponse); + + } + + + @Put('user-roles') + @Roles(OrgRoles.OWNER, OrgRoles.ADMIN) + @ApiBearerAuth() + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + @ApiResponse({ status: 200, description: 'Success', type: ApiResponseDto }) + @ApiOperation({ summary: 'Update user roles', description: 'update user roles' }) + async updateUserRoles(@Body() updateUserDto: UpdateUserRolesDto, @Res() res: Response): Promise { + + await this.organizationService.updateUserRoles(updateUserDto, updateUserDto.userId); + + const finalResponse: IResponseType = { + statusCode: HttpStatus.OK, + message: ResponseMessages.organisation.success.updateUserRoles + }; + + return res.status(HttpStatus.OK).json(finalResponse); + } + + @Get('/:id') + @ApiOperation({ summary: 'Get an organization', description: 'Get an organization' }) + @ApiResponse({ status: 200, description: 'Success', type: ApiResponseDto }) + @UseGuards(AuthGuard('jwt')) + @ApiBearerAuth() + async getOrganization(@Param('id') orgId: number, @Res() res: Response, @User() reqUser: user): Promise { + + const getOrganization = await this.organizationService.getOrganization(orgId, reqUser.id); + + const finalResponse: IResponseType = { + statusCode: HttpStatus.OK, + message: ResponseMessages.organisation.success.getOrganization, + data: getOrganization.response + }; + return res.status(HttpStatus.OK).json(finalResponse); + + } + + + @Put('/') + @ApiOperation({ summary: 'Update Organization', description: 'Update an organization' }) + @ApiResponse({ status: 200, description: 'Success', type: ApiResponseDto }) + @ApiBearerAuth() + @Roles(OrgRoles.OWNER, OrgRoles.ADMIN) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + async updateOrganization(@Body() updateOrgDto: UpdateOrganizationDto, @Res() res: Response): Promise { + + await this.organizationService.updateOrganization(updateOrgDto); + + const finalResponse: IResponseType = { + statusCode: HttpStatus.OK, + message: ResponseMessages.organisation.success.update + }; + return res.status(HttpStatus.OK).json(finalResponse); + } +} diff --git a/apps/api-gateway/src/organization/organization.module.ts b/apps/api-gateway/src/organization/organization.module.ts new file mode 100644 index 000000000..9357db1bd --- /dev/null +++ b/apps/api-gateway/src/organization/organization.module.ts @@ -0,0 +1,29 @@ +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 { OrganizationController } from './organization.controller'; +import { OrganizationService } from './organization.service'; + +@Module({ + imports: [ + HttpModule, + ConfigModule.forRoot(), + ClientsModule.register([ + { + name: 'NATS_CLIENT', + transport: Transport.NATS, + options: { + servers: [`${process.env.NATS_URL}`] + } + }, + CommonModule + ]) + ], + controllers: [OrganizationController], + providers: [OrganizationService, CommonService] +}) +export class OrganizationModule { } + diff --git a/apps/api-gateway/src/organization/organization.service.ts b/apps/api-gateway/src/organization/organization.service.ts new file mode 100644 index 000000000..35b6af328 --- /dev/null +++ b/apps/api-gateway/src/organization/organization.service.ts @@ -0,0 +1,133 @@ +import { Inject } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; +import { ClientProxy, RpcException } from '@nestjs/microservices'; +import { BaseService } from 'libs/service/base.service'; +import { CreateOrganizationDto } from './dtos/create-organization-dto'; +import { GetAllOrganizationsDto } from './dtos/get-all-organizations.dto'; +import { GetAllSentInvitationsDto } from './dtos/get-all-sent-invitations.dto'; +import { BulkSendInvitationDto } from './dtos/send-invitation.dto'; +import { UpdateUserRolesDto } from './dtos/update-user-roles.dto'; +import { UpdateOrganizationDto } from './dtos/update-organization-dto'; + +@Injectable() +export class OrganizationService extends BaseService { + + constructor( + @Inject('NATS_CLIENT') private readonly serviceProxy: ClientProxy + ) { + super('OrganizationService'); + } + + /** + * + * @param createOrgDto + * @returns Organization creation Success + */ + async createOrganization(createOrgDto: CreateOrganizationDto, userId: number): Promise { + try { + const payload = { createOrgDto, userId }; + return this.sendNats(this.serviceProxy, 'create-organization', payload); + } catch (error) { + this.logger.error(`In service Error: ${error}`); + throw new RpcException(error.response); + } + } + + /** + * + * @param updateOrgDto + * @returns Organization update Success + */ + async updateOrganization(updateOrgDto: UpdateOrganizationDto): Promise { + try { + const payload = { updateOrgDto }; + return this.sendNats(this.serviceProxy, 'update-organization', payload); + } catch (error) { + this.logger.error(`In service Error: ${error}`); + throw new RpcException(error.response); + } + } + + + /** + * + * @param + * @returns Organizations details + */ + async getOrganizations(getAllOrgsDto: GetAllOrganizationsDto, userId: number): Promise<{ response: object }> { + const payload = { userId, ...getAllOrgsDto }; + return this.sendNats(this.serviceProxy, 'get-organizations', payload); + } + + + /** + * + * @param orgId + * @returns Organization get Success + */ + async getOrganization(orgId: number, userId: number): Promise<{ response: object }> { + const payload = { orgId, userId }; + return this.sendNats(this.serviceProxy, 'get-organization-by-id', payload); + } + + /** + * + * @param orgId + * @returns Invitations details + */ + async getInvitationsByOrgId(orgId: number, getAllInvitationsDto: GetAllSentInvitationsDto): Promise<{ response: object }> { + const { pageNumber, pageSize, search } = getAllInvitationsDto; + const payload = { orgId, pageNumber, pageSize, search }; + return this.sendNats(this.serviceProxy, 'get-invitations-by-orgId', payload); + } + + async getOrganizationDashboard(orgId: number, userId: number): Promise<{ response: object }> { + const payload = { orgId, userId }; + return this.sendNats(this.serviceProxy, 'get-organization-dashboard', payload); + } + + /** + * + * @param + * @returns get organization roles + */ + async getOrgRoles(): Promise { + try { + const payload = {}; + return this.sendNats(this.serviceProxy, 'get-org-roles', payload); + } catch (error) { + this.logger.error(`In service Error: ${error}`); + throw new RpcException(error.response); + } + } + + + /** + * + * @param sendInvitationDto + * @returns Organization invitation creation Success + */ + async createInvitation(bulkInvitationDto: BulkSendInvitationDto, userId: number): Promise { + try { + const payload = { bulkInvitationDto, userId }; + return this.sendNats(this.serviceProxy, 'send-invitation', payload); + } catch (error) { + this.logger.error(`In service Error: ${error}`); + throw new RpcException(error.response); + } + } + + /** + * + * @param updateUserDto + * @param userId + * @returns User roles update response + */ + async updateUserRoles( + updateUserDto: UpdateUserRolesDto, + userId: number + ): Promise<{ response: boolean }> { + const payload = { orgId: updateUserDto.orgId, roleIds: updateUserDto.orgRoleId, userId }; + return this.sendNats(this.serviceProxy, 'update-user-roles', payload); + } +} diff --git a/apps/api-gateway/src/platform/platform.controller.spec.ts b/apps/api-gateway/src/platform/platform.controller.spec.ts new file mode 100644 index 000000000..8cb1a53a9 --- /dev/null +++ b/apps/api-gateway/src/platform/platform.controller.spec.ts @@ -0,0 +1,74 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { plainToClass } from 'class-transformer'; +import { validate } from 'class-validator'; +import { PlatformController } from './platform.controller'; +import { PlatformService } from './platform.service'; +import { SortValue } from './platform.model'; + +describe('Credentia lDefinitionController Test Cases', () => { + let controller: PlatformController; + const mockCredentialDefinitionService = { + connectedHolderList: jest.fn(() => ({})), + getCredentialListByConnectionId: jest.fn(() => ({})), + pingServicePlatform: jest.fn(() => ({})) + }; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [PlatformController], + providers: [PlatformService] + }) + .overrideProvider(PlatformService) + .useValue(mockCredentialDefinitionService) + .compile(); + controller = module.get( + PlatformController + ); + }); + + describe('connected holder list', () => { + const itemsPerPage: any = 1; + const page: any = 1; + const searchText: any = 'abc'; + const orgId: any = 13; + const connectionSortBy: any = 'xyz'; + const sortValue: any = 'asdd'; + it('should return an expected connected holder list', async () => { + const result = await controller.connectedHolderList( + itemsPerPage, + page, + searchText, + orgId, + connectionSortBy, + sortValue + ); + expect(result).toEqual({}); + }); + }); + describe('get credential list by connection id', () => { + const connectionId = 'jhkh'; + const itemsPerPage = 10; + const page = 1; + const searchText = 'abc'; + const sortValue:any = SortValue; + const credentialSortBy = 'ddcc'; + it('should return an expected credential list by connection id', async () => { + const result = await controller.getCredentialListByConnectionId( + connectionId, + itemsPerPage, + page, + searchText, + sortValue, + credentialSortBy + ); + expect(result).toEqual({}); + }); + }); + describe('get the platform service status', () => { + it('should return an expected platform service status', async () => { + const result = await controller.pingServicePlatform(); + expect(result).toEqual({}); + }); + }); + }); + \ No newline at end of file diff --git a/apps/api-gateway/src/platform/platform.controller.ts b/apps/api-gateway/src/platform/platform.controller.ts new file mode 100644 index 000000000..9cfc655c4 --- /dev/null +++ b/apps/api-gateway/src/platform/platform.controller.ts @@ -0,0 +1,13 @@ +import { Controller, Logger } from '@nestjs/common'; +import { PlatformService } from './platform.service'; +import { ApiBearerAuth } from '@nestjs/swagger'; + +@ApiBearerAuth() +@Controller('connections') +export class PlatformController { + constructor(private readonly platformService: PlatformService) { } + + private readonly logger = new Logger('PlatformController'); + +} + diff --git a/apps/api-gateway/src/platform/platform.interface.ts b/apps/api-gateway/src/platform/platform.interface.ts new file mode 100644 index 000000000..a348d9367 --- /dev/null +++ b/apps/api-gateway/src/platform/platform.interface.ts @@ -0,0 +1,33 @@ +import { credentialSortBy } from '../enum'; + +export enum SortValue { + ASC = 'ASC', + DESC = 'DESC' +} +export interface IConnectedHolderList { + orgId: number; + itemsPerPage?: number; + page?: number; + searchText?: string; + connectionSortBy?: string; + sortValue?: string; +} + + +export interface CredentialListPayload { + connectionId: string; + itemsPerPage: number; + page: number; + searchText: string; + sortValue: SortValue; + credentialSortBy: string; +} + +export interface GetCredentialListByConnectionId { + connectionId: string, + items_per_page: number, + page: number, + search_text: string, + sortValue: SortValue, + sortBy: credentialSortBy +} \ No newline at end of file diff --git a/apps/api-gateway/src/platform/platform.module.ts b/apps/api-gateway/src/platform/platform.module.ts new file mode 100644 index 000000000..ba0ffe2ec --- /dev/null +++ b/apps/api-gateway/src/platform/platform.module.ts @@ -0,0 +1,22 @@ +import { Module } from '@nestjs/common'; +import { PlatformController } from './platform.controller'; +import { PlatformService } from './platform.service'; +import { ClientsModule, Transport } from '@nestjs/microservices'; +import { ConfigModule } from '@nestjs/config'; +import { commonNatsOptions } from 'libs/service/nats.options'; + +@Module({ + imports: [ + ConfigModule.forRoot(), + ClientsModule.register([ + { + name: 'NATS_CLIENT', + ...commonNatsOptions('AGENT_SERVICE:REQUESTER') + } + ]) + + ], + controllers: [PlatformController], + providers: [PlatformService] +}) +export class PlatformModule {} diff --git a/apps/api-gateway/src/platform/platform.service.ts b/apps/api-gateway/src/platform/platform.service.ts new file mode 100644 index 000000000..54a3bb353 --- /dev/null +++ b/apps/api-gateway/src/platform/platform.service.ts @@ -0,0 +1,65 @@ +import { Injectable, Inject, Logger, HttpException } from '@nestjs/common'; +import { ClientProxy } from '@nestjs/microservices'; +import { BaseService } from '../../../../libs/service/base.service'; +import { map } from 'rxjs/operators'; +import { CredentialListPayload, GetCredentialListByConnectionId, IConnectedHolderList, SortValue } from './platform.interface'; +import { ConnectionDto } from '../dtos/connection.dto'; +import { credentialSortBy } from '../enum'; + +@Injectable() +export class PlatformService extends BaseService { + constructor( + @Inject('NATS_CLIENT') private readonly platformServiceProxy: ClientProxy + ) { + super('PlatformService'); + } + + + /** + * Description: Calling platform service for connection-invitation + * @param alias + * @param auto_accept + * @param _public + * @param multi_use + */ + createConnectionInvitation(alias: string, auto_accept: boolean, _public: boolean, multi_use: boolean) { + this.logger.log('**** createConnectionInvitation called...'); + const payload = { alias, auto_accept, _public, multi_use }; + return this.sendNats(this.platformServiceProxy, 'default-connection-invitation', payload); + } + + /** + * Description: Calling platform service for connection-list + * @param alias + * @param initiator + * @param invitation_key + * @param my_did + * @param state + * @param their_did + * @param their_role + */ + getConnections(alias: string, initiator: string, invitation_key: string, my_did: string, state: string, their_did: string, their_role: string, user: any) { + this.logger.log('**** getConnections called...'); + const payload = { alias, initiator, invitation_key, my_did, state, their_did, their_role, user }; + return this.sendNats(this.platformServiceProxy, 'connection-list', payload); + } + + pingServicePlatform() { + this.logger.log('**** pingServicePlatform called...'); + const payload = {}; + return this.sendNats(this.platformServiceProxy, 'ping-platform', payload); + } + + + connectedHolderList(itemsPerPage: number, page: number, searchText: string, orgId: number, connectionSortBy: string, sortValue: string) { + this.logger.log('**** connectedHolderList called...'); + const payload: IConnectedHolderList = { itemsPerPage, page, searchText, orgId, connectionSortBy, sortValue }; + return this.sendNats(this.platformServiceProxy, 'connected-holder-list', payload); + } + + getCredentialListByConnectionId(connectionId: string, items_per_page: number, page: number, search_text: string, sortValue: SortValue, sortBy: credentialSortBy) { + this.logger.log('**** getCredentialListByConnectionId called...'); + const payload:GetCredentialListByConnectionId = { connectionId, items_per_page, page, search_text, sortValue, sortBy }; + return this.sendNats(this.platformServiceProxy, 'get-credential-by-connection-id', payload); + } +} diff --git a/apps/api-gateway/src/revocation/revocation.controller.ts b/apps/api-gateway/src/revocation/revocation.controller.ts new file mode 100644 index 000000000..c04a81898 --- /dev/null +++ b/apps/api-gateway/src/revocation/revocation.controller.ts @@ -0,0 +1,79 @@ +import { Controller, Logger, Post, Body, UseGuards, Patch, Param, Get, Query } from '@nestjs/common'; +import { ApiTags, ApiResponse, ApiBearerAuth, ApiQuery, ApiOperation } from '@nestjs/swagger'; +import { CreateRevocationRegistryDto } from '../dtos/create-revocation-registry.dto'; +import { RevocationService } from './revocation.service'; +import { GetUser } from '../authz/decorators/get-user.decorator'; +import { AuthGuard } from '@nestjs/passport'; +import { UpdateRevocationRegistryUriDto } from '../dtos/update-revocation-registry.dto'; + +@ApiBearerAuth() +@Controller() +export class RevocationController { + + private readonly logger = new Logger('RevocationController'); + + constructor(private readonly revocationService: RevocationService) { } + + // @UseGuards(AuthGuard('jwt')) + // @Post('/revocation/create-registry') + // @ApiTags('revocation-registry') + // @ApiOperation({ summary: 'Creates a new revocation registry' }) + // @ApiResponse({ status: 201, description: 'Create Revocation Registry' }) + // createRevocationRegistry( + // @Body() createRevocationRegistryDto: CreateRevocationRegistryDto, + // @GetUser() user: any + // ) { + // return this.revocationService.createRevocationRegistry(createRevocationRegistryDto, user); + // } + + // @UseGuards(AuthGuard('jwt')) + // @Post('/revocation/registry/update-uri') + // @ApiTags('revocation-registry') + // @ApiOperation({ summary: 'Update revocation registry with new public URI to the tails file.' }) + // @ApiResponse({ status: 201, description: 'Update revocation registry with new public URI to the tails file.' }) + // updateRevocationRegistryUri( + // @Body() updateRevocationRegistryUriDto: UpdateRevocationRegistryUriDto, + // @GetUser() user: any + // ) { + // return this.revocationService.updateRevocationRegistryUri(updateRevocationRegistryUriDto, user); + // } + + // @UseGuards(AuthGuard('jwt')) + // @Get('/revocation/active-registry') + // @ApiTags('revocation-registry') + // @ApiQuery({ name: 'cred_def_id', required: true }) + // @ApiOperation({ summary: 'Get an active revocation registry by credential definition id' }) + // @ApiResponse({ status: 200, description: 'Get an active revocation registry by credential definition id' }) + // activeRevocationRegistry( + // @Query('cred_def_id') cred_def_id: string, + // @GetUser() user: any + // ) { + // return this.revocationService.activeRevocationRegistry(cred_def_id, user); + // } + + // @UseGuards(AuthGuard('jwt')) + // @Post('/revocation/registry/publish') + // @ApiQuery({ name: 'rev_reg_id', required: true }) + // @ApiTags('revocation-registry') + // @ApiOperation({ summary: 'Publish a given revocation registry' }) + // @ApiResponse({ status: 201, description: 'Publish a given revocation registry' }) + // publishRevocationRegistry( + // @Query('rev_reg_id') revocationId: string, + // @GetUser() user: any + // ) { + // return this.revocationService.publishRevocationRegistry(revocationId, user); + // } + + // @UseGuards(AuthGuard('jwt')) + // @Get('/revocation/registry') + // @ApiTags('revocation-registry') + // @ApiQuery({ name: 'rev_reg_id', required: true }) + // @ApiOperation({ summary: 'Get revocation registry by revocation registry id' }) + // @ApiResponse({ status: 200, description: 'Get revocation registry by revocation registry id' }) + // getRevocationRegistry( + // @Query() rev_reg_id: string, + // @GetUser() user: any + // ) { + // return this.revocationService.getRevocationRegistry(rev_reg_id, user); + // } +} \ No newline at end of file diff --git a/apps/api-gateway/src/revocation/revocation.module.ts b/apps/api-gateway/src/revocation/revocation.module.ts new file mode 100644 index 000000000..249e7a127 --- /dev/null +++ b/apps/api-gateway/src/revocation/revocation.module.ts @@ -0,0 +1,23 @@ +import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { Transport, ClientsModule } from '@nestjs/microservices'; +import { RevocationService } from './revocation.service'; +import { RevocationController } from './revocation.controller'; +import { commonNatsOptions } from 'libs/service/nats.options'; + +@Module({ + imports: [ + ConfigModule.forRoot(), + ClientsModule.register([ + { + name: 'NATS_CLIENT', + ...commonNatsOptions('REVOCATION_SERVICE:REQUESTER') + } + ]) + ], + controllers: [RevocationController], + providers: [RevocationService] +}) +export class RevocationModule { + +} diff --git a/apps/api-gateway/src/revocation/revocation.service.ts b/apps/api-gateway/src/revocation/revocation.service.ts new file mode 100644 index 000000000..3f9ffd1b7 --- /dev/null +++ b/apps/api-gateway/src/revocation/revocation.service.ts @@ -0,0 +1,46 @@ +import { Injectable, Inject, Logger, HttpException } from '@nestjs/common'; +import { ClientProxy } from '@nestjs/microservices'; +import { CreateRevocationRegistryDto } from '../dtos/create-revocation-registry.dto'; +import { map } from 'rxjs/operators'; +import { UpdateRevocationRegistryUriDto } from '../dtos/update-revocation-registry.dto'; +import { BaseService } from 'libs/service/base.service'; + + +@Injectable() +export class RevocationService extends BaseService { + constructor( + @Inject('NATS_CLIENT') private readonly revocationServiceProxy: ClientProxy + ) { + super('RevocationService'); + } + + createRevocationRegistry(createRevocationRegistryDto: CreateRevocationRegistryDto, user: any) { + this.logger.log('**** createRevocationRegistryDto called'); + const payload = { createRevocationRegistryDto, user }; + return this.sendNats(this.revocationServiceProxy, 'create-revocation-registry', payload); + } + + updateRevocationRegistryUri(updateRevocationRegistryUriDto: UpdateRevocationRegistryUriDto, user: any) { + this.logger.log('**** updateRevocationRegistryUri called'); + const payload = { updateRevocationRegistryUriDto, user }; + return this.sendNats(this.revocationServiceProxy, 'update-revocation-registry-uri', payload); + } + + activeRevocationRegistry(cred_def_id: string, user: any) { + this.logger.log('**** activeRevocationRegistry called'); + const payload = { cred_def_id, user }; + return this.sendNats(this.revocationServiceProxy, 'active-revocation-registry', payload); + } + + publishRevocationRegistry(revocationId: string, user: any) { + this.logger.log('**** publishRevocationRegistry called'); + const payload = { revocationId, user }; + return this.sendNats(this.revocationServiceProxy, 'publish-revocation-registry', payload); + } + + getRevocationRegistry(rev_reg_id: string, user: any) { + this.logger.log('**** getRevocationRegistry called'); + const payload = { rev_reg_id, user }; + return this.sendNats(this.revocationServiceProxy, 'get-revocation-registry', payload); + } +} \ No newline at end of file diff --git a/apps/api-gateway/src/schema/dtos/create-schema.dto.ts b/apps/api-gateway/src/schema/dtos/create-schema.dto.ts new file mode 100644 index 000000000..e70bfda62 --- /dev/null +++ b/apps/api-gateway/src/schema/dtos/create-schema.dto.ts @@ -0,0 +1,28 @@ +import { IsArray, IsNotEmpty, IsNumber, IsString } from 'class-validator'; + +import { ApiProperty } from '@nestjs/swagger'; + +export class CreateSchemaDto { + @ApiProperty() + @IsString({ message: 'schema version must be a string' }) @IsNotEmpty({ message: 'please provide valid schema version' }) + schemaVersion: string; + + @ApiProperty() + @IsString({ message: 'schema name must be a string' }) @IsNotEmpty({ message: 'please provide valid schema name' }) + schemaName: string; + + @ApiProperty() + @IsArray({ message: 'attributes must be an array' }) + @IsString({ each: true }) + @IsNotEmpty({ message: 'please provide valid attributes' }) + attributes: string[]; + + @ApiProperty() + @IsNumber() + @IsNotEmpty({ message: 'please provide orgId' }) + orgId: number; + + @ApiProperty() + @IsString({ message: 'orgDid must be a string' }) @IsNotEmpty({ message: 'please provide valid orgDid' }) + orgDid: string; +} diff --git a/apps/api-gateway/src/schema/dtos/get-all-schema.dto.ts b/apps/api-gateway/src/schema/dtos/get-all-schema.dto.ts new file mode 100644 index 000000000..b9552c4c8 --- /dev/null +++ b/apps/api-gateway/src/schema/dtos/get-all-schema.dto.ts @@ -0,0 +1,68 @@ +/* eslint-disable @typescript-eslint/no-inferrable-types */ +/* eslint-disable camelcase */ +import { ApiProperty } from '@nestjs/swagger'; +import { SortValue } from '../../enum'; +import { Transform, Type } from 'class-transformer'; +import { trim } from '@credebl/common/cast.helper'; +import { IsNotEmpty, IsNumber, IsOptional } from 'class-validator'; + +export class GetAllSchemaDto { + @ApiProperty({ required: false }) + @IsOptional() + @Type(() => Number) + @Transform(({ value }) => trim(value)) + pageNumber: number = 1; + + @ApiProperty({ required: false }) + @IsOptional() + @Type(() => String) + @Transform(({ value }) => trim(value)) + searchByText: string = ''; + + @ApiProperty({ required: false }) + @IsOptional() + @Type(() => Number) + @Transform(({ value }) => trim(value)) + pageSize: number = 10; + + @ApiProperty({ required: false }) + @IsOptional() + @Transform(({ value }) => trim(value)) + sorting: string = 'id'; + + @ApiProperty({ required: false }) + @IsOptional() + sortByValue: string = SortValue.DESC; + + @ApiProperty({ required: true }) + @Type(() => Number) + @IsNumber() + @IsNotEmpty() + orgId: number; +} + +export class GetCredentialDefinitionBySchemaIdDto { + @ApiProperty({ required: false }) + @IsOptional() + @Type(() => Number) + pageNumber: number = 1; + + @ApiProperty({ required: false }) + @IsOptional() + @Type(() => Number) + pageSize: number = 10; + + @ApiProperty({ required: false }) + @IsOptional() + sorting: string = 'id'; + + @ApiProperty({ required: false }) + @IsOptional() + sortByValue: string = SortValue.DESC; + + @ApiProperty({ required: true }) + @Type(() => Number) + @IsNumber() + @IsNotEmpty() + orgId: number; +} diff --git a/apps/api-gateway/src/schema/interfaces/index.ts b/apps/api-gateway/src/schema/interfaces/index.ts new file mode 100644 index 000000000..650f24143 --- /dev/null +++ b/apps/api-gateway/src/schema/interfaces/index.ts @@ -0,0 +1,41 @@ +import { UserRoleOrgPermsDto } from '../../dtos/user-role-org-perms.dto'; + +export interface IUserRequestInterface { + userId: number; + email: string; + orgId: number; + agentEndPoint?: string; + apiKey?: string; + tenantId?: number; + tenantName?: string; + tenantOrgId?: number; + userRoleOrgPermissions?: UserRoleOrgPermsDto[]; + orgName?: string; + selectedOrg: ISelectedOrgInterface; +} + +export interface ISelectedOrgInterface { + id: number; + userId: number; + orgRoleId: number; + orgId: number; + orgRole: object; + organisation: object; +} + +export interface IOrganizationInterface { + name: string; + description: string; + org_agents: IOrgAgentInterface[] + +} + +export interface IOrgAgentInterface { + orgDid: string; + verkey: string; + agentEndPoint: string; + agentOptions: string; + walletName: string; + agentsTypeId: string; + orgId: string; +} \ No newline at end of file diff --git a/apps/api-gateway/src/schema/schema.controller.spec.ts b/apps/api-gateway/src/schema/schema.controller.spec.ts new file mode 100644 index 000000000..6e0d602bc --- /dev/null +++ b/apps/api-gateway/src/schema/schema.controller.spec.ts @@ -0,0 +1,222 @@ +import { Test, TestingModule } from '@nestjs/testing'; + +import { Any } from 'typeorm'; +import { CreateSchemaDto } from '../dtos/create-schema.dto'; +import { SchemaController } from './schema.controller'; +import { SchemaService } from './schema.service'; +import { plainToClassFromExist } from 'class-transformer'; +import { validate } from 'class-validator'; +import { CredDefSortBy, SortValue } from '../enum'; + +describe('schemaController Test Cases', () => { + let controller: SchemaController; + const mockSchemaService = { + createSchema: jest.fn(() => ({})), + getSchemas: jest.fn(() => ({})), + getSchemaByOrg: jest.fn(() => ({})), + getSchemaBySchemaId:jest.fn(() => ({})), + getCredDefBySchemaId: jest.fn(() => ({})) + }; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [SchemaController], + providers: [SchemaService] + }) + .overrideProvider(SchemaService) + .useValue(mockSchemaService) + .compile(); + controller = module.get( + SchemaController + ); + }); + /////////////////////--------Create Schema -------------------////////////////////// + describe('createSchema', () => { + const user: any = {}; + + it('should return an expected schema', async () => { + const createSchemaDto: CreateSchemaDto = new CreateSchemaDto; + const result = await controller.createSchema( + createSchemaDto, + user + ); + expect(result).toEqual({}); + }); + /////////////// + + it('should check returned credentialdefinition is not to be null', async () => { + const createSchemaDto: CreateSchemaDto = new CreateSchemaDto; + const result = await controller.createSchema( + createSchemaDto, + user + ); + expect(result).not.toBeNull(); + }); + + //////////// + it('Should throw error when schema version is not string', async () => { + const schema_version_dto = { schema_version: 1 }; + const schemaVersionResult = plainToClassFromExist( + CreateSchemaDto, + schema_version_dto + ); + const errors = await validate(schemaVersionResult); + const result = await errors[0].constraints.isString; + expect(result).toEqual('schema version must be a string'); + }); + + + it('Should throw error when schema name is not a string', async () => { + const schemaName = { schema_name: 234 }; + const schemaNameResult = plainToClassFromExist( + CreateSchemaDto, + schemaName + ); + const errors = await validate(schemaNameResult); + const result = await errors[1].constraints.isString; + expect(result).toEqual('schema name must be a string'); + }); + + ////////////////////// + + it('should throw error when schema version is null', async () => { + const schema_version_dto = { schema_version: '' }; + const schemaNameResult = plainToClassFromExist( + CreateSchemaDto, + schema_version_dto + ); + const errors = await validate(schemaNameResult); + const result = await errors[0].constraints.isNotEmpty; + expect(result).toEqual('please provide valid schema version'); + }); + + it('should throw error when schema name is null', async () => { + const schema_name_dto = { schema_name: '' }; + const schemaNameResult = plainToClassFromExist( + CreateSchemaDto, + schema_name_dto + ); + const errors = await validate(schemaNameResult); + const result = await errors[1].constraints.isNotEmpty; + expect(result).toEqual('please provide valid schema name'); + }); + + ////////////////////// + + it('should throw error when attributes are not in array', async () => { + const attribute_dto = {attributes:'testAcb'}; + const schemaattributeResult = plainToClassFromExist( + CreateSchemaDto, + attribute_dto + ); + const errors = await validate(schemaattributeResult); + const result = await errors[2].constraints.isArray; + expect(result).toEqual('attributes must be an array'); + }); + + it('should throw error when elements of attributes are not a string', async () => { + const attribute_dto = {attributes: true}; + const schemaattributeResult = plainToClassFromExist( + CreateSchemaDto, + attribute_dto + ); + const errors = await validate(schemaattributeResult); + const result = await errors[2].constraints.isString; + expect(result).toEqual('each value in attributes must be a string'); + }); + + it('should throw error when attributes are null', async () => { + const attribute_dto = {attributes:''}; + const schemaattributeResult = plainToClassFromExist( + CreateSchemaDto, + attribute_dto + ); + const errors = await validate(schemaattributeResult); + const result = await errors[2].constraints.isNotEmpty; + expect(result).toEqual('please provide valid attributes'); + }); + + }); + +///////////////////// --- get schema by ledger Id ------------/////////// + + + describe('getSchemas', () => { + const user: any = {}; + const page = 1; + const search_text = 'test search'; + const items_per_page = 1; + const schemaSortBy = 'id'; + const sortValue = 'DESC'; + it('should return an expected schemas by ledger Id', async () => { + const result = await controller.getSchemaWithFilters( + page, + search_text, + items_per_page, + schemaSortBy, + sortValue, + user + ); + expect(result).toEqual({}); + }); + + }); + +/////////////////////////----- get schemas with organization id ---------- + describe('getSchemasByOrgId', () => { + const user: any = {}; + const page = 1; + const search_text = 'test search'; + const items_per_page = 1; + const sortValue = 1; + const id = 1; + const schemaSortBy = 'DESC'; + it('should return an expected schemas by org Id', async () => { + const result = await controller.getSchemasByOrgId( + page, + search_text, + items_per_page, + sortValue, + schemaSortBy, + id + ); + expect(result).toEqual({}); + }); + }); + +/////////////////////------- get schemas with schema ledger Id --------------- + + describe('getSchemaBySchemaId', () => { + const user: any = {}; + const id = '1'; + it('should return an expected schemas by org Id', async () => { + const result = await controller.getSchemaById( + user, + id + ); + expect(result).toEqual({}); + }); + }); + +/////////--------- get cred defs with schema Id ---/// + describe('getCredDefBySchemaId', () => { + const user: any = {}; + const id = 1; + const page = 1; + const search_text = 'test'; + const items_per_page = 1; + const orgId = 2; + const credDefSortBy = CredDefSortBy.id; + const sortValue = SortValue.DESC; + const supportRevocation = 'all'; + it('should return expected cred defs by schemaId', async () => { + const result = await controller.getCredDefBySchemaId( + page, search_text, items_per_page, orgId, credDefSortBy, sortValue, supportRevocation, id, user + ); + expect(result).toEqual({}); + }); + + }); + + +}); \ No newline at end of file diff --git a/apps/api-gateway/src/schema/schema.controller.ts b/apps/api-gateway/src/schema/schema.controller.ts new file mode 100644 index 000000000..832b37e2b --- /dev/null +++ b/apps/api-gateway/src/schema/schema.controller.ts @@ -0,0 +1,151 @@ +import { Controller, Logger, Post, Body, HttpStatus, UseGuards, Get, Query, BadRequestException, Res } from '@nestjs/common'; +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable camelcase */ +import { ApiOperation, ApiResponse, ApiTags, ApiBearerAuth, ApiForbiddenResponse, ApiUnauthorizedResponse, ApiQuery } from '@nestjs/swagger'; +import { SchemaService } from './schema.service'; +import { AuthGuard } from '@nestjs/passport'; +import { ApiResponseDto } from '../dtos/apiResponse.dto'; +import { UnauthorizedErrorDto } from '../dtos/unauthorized-error.dto'; +import { ForbiddenErrorDto } from '../dtos/forbidden-error.dto'; +import IResponseType from '@credebl/common/interfaces/response.interface'; +import { Response } from 'express'; +import { User } from '../authz/decorators/user.decorator'; +import { ICredDeffSchemaSearchInterface, ISchemaSearchInterface } from '../interfaces/ISchemaSearch.interface'; +import { ResponseMessages } from '@credebl/common/response-messages'; +import { GetAllSchemaDto, GetCredentialDefinitionBySchemaIdDto } from './dtos/get-all-schema.dto'; +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'; + +@Controller('schemas') +@ApiTags('schemas') +@Roles(OrgRoles.OWNER, OrgRoles.SUPER_ADMIN, OrgRoles.ADMIN, OrgRoles.ISSUER) +@UseGuards(AuthGuard('jwt'), OrgRolesGuard) +@ApiBearerAuth() +@ApiUnauthorizedResponse({ status: 401, description: 'Unauthorized', type: UnauthorizedErrorDto }) +@ApiForbiddenResponse({ status: 403, description: 'Forbidden', type: ForbiddenErrorDto }) +export class SchemaController { + constructor(private readonly appService: SchemaService + ) { } + private readonly logger = new Logger('SchemaController'); + + @Post('/') + @ApiOperation({ + summary: 'Sends a schema to the ledger', + description: 'Create and sends a schema to the ledger.' + }) + @ApiResponse({ status: 201, description: 'Success', type: ApiResponseDto }) + async createSchema(@Res() res: Response, @Body() schema: CreateSchemaDto, @User() user: IUserRequestInterface): Promise { + schema.attributes.forEach((attribute) => { + if (0 === attribute.length) { + throw new BadRequestException('Attribute must not be empty'); + } else if ('' === attribute.trim()) { + throw new BadRequestException('Attributes should not contain space'); + } + }); + const schemaDetails = await this.appService.createSchema(schema, user, schema.orgId); + + const finalResponse: IResponseType = { + statusCode: HttpStatus.CREATED, + message: 'Schema created successfully', + data: schemaDetails.response + }; + return res.status(HttpStatus.CREATED).json(finalResponse); + } + + @Get('/') + @ApiOperation({ + summary: 'Get all schemas', + description: 'Get all schemas.' + }) + @ApiResponse({ status: 200, description: 'Success', type: ApiResponseDto }) + async getSchemas( + @Query() getAllSchemaDto: GetAllSchemaDto, + @Res() res: Response, + @User() user: IUserRequestInterface + ): Promise { + const { orgId, pageSize, searchByText, pageNumber, sorting, sortByValue } = getAllSchemaDto; + const schemaSearchCriteria: ISchemaSearchInterface = { + pageNumber, + searchByText, + pageSize, + sorting, + sortByValue + }; + const schemasResponse = await this.appService.getSchemas(schemaSearchCriteria, user, orgId); + + const finalResponse: IResponseType = { + statusCode: HttpStatus.OK, + message: ResponseMessages.schema.success.fetch, + data: schemasResponse.response + }; + return res.status(HttpStatus.OK).json(finalResponse); + } + + @Get('/id') + @ApiOperation({ + summary: 'Get an existing schema by schemaId', + description: 'Get an existing schema by schemaId' + }) + @ApiQuery( + { name: 'schemaId', required: true } + ) + + @ApiQuery( + { name: 'orgId', required: true } + ) + @ApiResponse({ status: 200, description: 'Success', type: ApiResponseDto }) + async getSchemaById( + @Query('schemaId') schemaId: string, + @Query('orgId') orgId: number, + @Res() res: Response): Promise { + if (!schemaId) { + throw new BadRequestException(ResponseMessages.schema.error.invalidSchemaId); + } + const schemaDetails = await this.appService.getSchemaById(schemaId, orgId); + const finalResponse: IResponseType = { + statusCode: HttpStatus.OK, + message: ResponseMessages.schema.success.fetch, + data: schemaDetails.response + }; + return res.status(HttpStatus.OK).json(finalResponse); + } + + @Get('/credential-definitions') + @ApiOperation({ + summary: 'Get an existing credential definition list by schemaId', + description: 'Get an existing credential definition list by schemaId' + }) + @ApiQuery( + { name: 'schemaId', required: true } + ) + @ApiQuery( + { name: 'orgId', required: true } + ) + @ApiResponse({ status: 200, description: 'Success', type: ApiResponseDto }) + async getcredDeffListBySchemaId( + @Query('schemaId') schemaId: string, + @Query() GetCredentialDefinitionBySchemaIdDto: GetCredentialDefinitionBySchemaIdDto, + @Res() res: Response, + @User() user: IUserRequestInterface): Promise { + if (!schemaId) { + throw new BadRequestException(ResponseMessages.schema.error.invalidSchemaId); + } + const { orgId, pageSize, pageNumber, sorting, sortByValue } = GetCredentialDefinitionBySchemaIdDto; + const schemaSearchCriteria: ICredDeffSchemaSearchInterface = { + pageNumber, + pageSize, + sorting, + sortByValue + }; + const credentialDefinitionList = await this.appService.getcredDeffListBySchemaId(schemaId, schemaSearchCriteria, user, orgId); + const finalResponse: IResponseType = { + statusCode: HttpStatus.OK, + message: ResponseMessages.schema.success.fetch, + data: credentialDefinitionList.response + }; + return res.status(HttpStatus.OK).json(finalResponse); + } +} diff --git a/apps/api-gateway/src/schema/schema.module.ts b/apps/api-gateway/src/schema/schema.module.ts new file mode 100644 index 000000000..c1649fe9d --- /dev/null +++ b/apps/api-gateway/src/schema/schema.module.ts @@ -0,0 +1,24 @@ +import { ClientsModule, Transport } from '@nestjs/microservices'; + +import { ConfigModule } from '@nestjs/config'; +import { Module } from '@nestjs/common'; +import { SchemaController } from './schema.controller'; +import { SchemaService } from './schema.service'; + +@Module({ + imports: [ + ConfigModule.forRoot(), + ClientsModule.register([ + { + name: 'NATS_CLIENT', + transport: Transport.NATS, + options: { + servers: [`${process.env.NATS_URL}`] + } + } + ]) + ], + controllers: [SchemaController], + providers: [SchemaService] +}) +export class SchemaModule { } diff --git a/apps/api-gateway/src/schema/schema.service.ts b/apps/api-gateway/src/schema/schema.service.ts new file mode 100644 index 000000000..03099b86a --- /dev/null +++ b/apps/api-gateway/src/schema/schema.service.ts @@ -0,0 +1,62 @@ +import { Injectable, Inject } from '@nestjs/common'; +import { ClientProxy, RpcException } from '@nestjs/microservices'; +import { BaseService } from '../../../../libs/service/base.service'; +import { CreateSchemaDto } from '../dtos/create-schema.dto'; +import { ICredDeffSchemaSearchInterface, ISchemaSearchInterface } from '../interfaces/ISchemaSearch.interface'; +import { IUserRequestInterface } from './interfaces'; + +@Injectable() +export class SchemaService extends BaseService { + + constructor( + @Inject('NATS_CLIENT') private readonly schemaServiceProxy: ClientProxy + ) { super(`Schema Service`); } + + createSchema(schema: CreateSchemaDto, user: IUserRequestInterface, orgId: number): Promise<{ + response: object; + }> { + try { + const payload = { schema, user, orgId }; + return this.sendNats(this.schemaServiceProxy, 'create-schema', payload); + } catch (error) { + throw new RpcException(error.response); + + } + } + + getSchemaById(schemaId: string, orgId: number): Promise<{ + response: object; + }> { + try { + const payload = { schemaId, orgId }; + return this.sendNats(this.schemaServiceProxy, 'get-schema-by-id', payload); + } catch (error) { + throw new RpcException(error.response); + + } + } + + getSchemas(schemaSearchCriteria: ISchemaSearchInterface, user: IUserRequestInterface, orgId: number): Promise<{ + response: object; + }> { + try { + const schemaSearch = { schemaSearchCriteria, user, orgId }; + return this.sendNats(this.schemaServiceProxy, 'get-schemas', schemaSearch); + } catch (error) { + throw new RpcException(error.response); + + } + } + + getcredDeffListBySchemaId(schemaId:string, schemaSearchCriteria: ICredDeffSchemaSearchInterface, user: IUserRequestInterface, orgId: number): Promise<{ + response: object; + }> { + try { + const payload = { schemaId, schemaSearchCriteria, user, orgId }; + return this.sendNats(this.schemaServiceProxy, 'get-cred-deff-list-by-schemas-id', payload); + } catch (error) { + throw new RpcException(error.response); + + } + } +} diff --git a/apps/api-gateway/src/secrets/13b0e1df87be249a.pem b/apps/api-gateway/src/secrets/13b0e1df87be249a.pem new file mode 100644 index 000000000..b499c7492 --- /dev/null +++ b/apps/api-gateway/src/secrets/13b0e1df87be249a.pem @@ -0,0 +1,36 @@ +-----BEGIN CERTIFICATE----- +MIIGOzCCBSOgAwIBAgIIE7Dh34e+JJowDQYJKoZIhvcNAQELBQAwgbQxCzAJBgNV +BAYTAlVTMRAwDgYDVQQIEwdBcml6b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMRow +GAYDVQQKExFHb0RhZGR5LmNvbSwgSW5jLjEtMCsGA1UECxMkaHR0cDovL2NlcnRz +LmdvZGFkZHkuY29tL3JlcG9zaXRvcnkvMTMwMQYDVQQDEypHbyBEYWRkeSBTZWN1 +cmUgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IC0gRzIwHhcNMjAwNzIwMTg1NjI1WhcN +MjEwNzIwMTg1NjI1WjA9MSEwHwYDVQQLExhEb21haW4gQ29udHJvbCBWYWxpZGF0 +ZWQxGDAWBgNVBAMMDyouaWRzd2FsbGV0LmNvbTCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBALvSaTJO9WG/6YiMCp9qmbPp8deuJ2f8QJCoH+PhlyqS4kaG +lRPoU/VWeVVAr2F6qbpzEcDaVDcLFFGf7JLIFobUqHPM1jFQYbvsIwcf9NsZ/8+P +4ur6HXfM5NuRuiz/5C3yaPM0U5kAoKlvoiJbORdttazHESDJ2nRzAi7ZrJ6yX74I +v5Gt1WgkT80PV8oJD0VGXgNPHomZ4R66uVT3NrlEsPdjCT+IYNzef4YsysVcstlY +JPfYpmbdAucY4jEFKoFgkuwW4rvGxoftbvLl9U4MTRd1FL7FtddrhxXTC/O66KD3 +FaS9wUmDnI0PNEkkN8ypv8UCjPOqU7jx5T5YRZ8CAwEAAaOCAsUwggLBMAwGA1Ud +EwEB/wQCMAAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMA4GA1UdDwEB +/wQEAwIFoDA4BgNVHR8EMTAvMC2gK6AphidodHRwOi8vY3JsLmdvZGFkZHkuY29t +L2dkaWcyczEtMjEzOC5jcmwwXQYDVR0gBFYwVDBIBgtghkgBhv1tAQcXATA5MDcG +CCsGAQUFBwIBFitodHRwOi8vY2VydGlmaWNhdGVzLmdvZGFkZHkuY29tL3JlcG9z +aXRvcnkvMAgGBmeBDAECATB2BggrBgEFBQcBAQRqMGgwJAYIKwYBBQUHMAGGGGh0 +dHA6Ly9vY3NwLmdvZGFkZHkuY29tLzBABggrBgEFBQcwAoY0aHR0cDovL2NlcnRp +ZmljYXRlcy5nb2RhZGR5LmNvbS9yZXBvc2l0b3J5L2dkaWcyLmNydDAfBgNVHSME +GDAWgBRAwr0njsw0gzCiM9f7bLPwtCyAzjApBgNVHREEIjAggg8qLmlkc3dhbGxl +dC5jb22CDWlkc3dhbGxldC5jb20wHQYDVR0OBBYEFMT3bmcO7H2AdnMBGIc+IjSP +MvuNMIIBBAYKKwYBBAHWeQIEAgSB9QSB8gDwAHYA9lyUL9F3MCIUVBgIMJRWjuNN +Exkzv98MLyALzE7xZOMAAAFzbZWd1QAABAMARzBFAiAICn4Mo2AXSzEX/EFfU+2D +WxOv9r0YUz2hyHwbW7jgjAIhAIH4POyAgBRXWqXXX7l/6l+zGi8s1EnSZ59MIl2o +nxmsAHYAXNxDkv7mq0VEsV6a1FbmEDf71fpH3KFzlLJe5vbHDsoAAAFzbZWfAQAA +BAMARzBFAiEAgKctaNh6KE76cGShExtrT46hKvVnTf4SB3MUlvTyxKsCIA9WDBGE +rOHFJVj8+luFkY34GSvTkgLlVZr8fhSZfd6AMA0GCSqGSIb3DQEBCwUAA4IBAQBa +VS457jYfh2skILPdoQZRFgHvzkVhbiviqjIdrk0j2Fwp7KK0iw0HLHwmFEU1g4Ss ++DCRBRq1KRmS4DVSUu6Crel3KUOCfA3Xl+Ck9ceS66Mcj2B+Bapi54Iew+qu2y7P +RHUfsqp9IvATZm/PC/H+omzPYw/u2HykWQwhbGe6isN9nDhkQfH2YG3DfQF75zfi +kZMVWYBO76ZDS88r7uJ3UGKgbixxxTK2KtFc/WdxMeRrazekYtOO/aZNCBuzMWUT +y9EgKfF6ufp7MdJDxtJd3z9WVfkJik48bBToA0uN908y4UqIOHqs42Gx5L5jKGDD +4baTF3+3G/2fWVEeDIvv +-----END CERTIFICATE----- diff --git a/apps/api-gateway/src/secrets/idswallet.key b/apps/api-gateway/src/secrets/idswallet.key new file mode 100644 index 000000000..ad24f0ff1 --- /dev/null +++ b/apps/api-gateway/src/secrets/idswallet.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC70mkyTvVhv+mI +jAqfapmz6fHXridn/ECQqB/j4ZcqkuJGhpUT6FP1VnlVQK9heqm6cxHA2lQ3CxRR +n+ySyBaG1KhzzNYxUGG77CMHH/TbGf/Pj+Lq+h13zOTbkbos/+Qt8mjzNFOZAKCp +b6IiWzkXbbWsxxEgydp0cwIu2ayesl++CL+RrdVoJE/ND1fKCQ9FRl4DTx6JmeEe +urlU9za5RLD3Ywk/iGDc3n+GLMrFXLLZWCT32KZm3QLnGOIxBSqBYJLsFuK7xsaH +7W7y5fVODE0XdRS+xbXXa4cV0wvzuuig9xWkvcFJg5yNDzRJJDfMqb/FAozzqlO4 +8eU+WEWfAgMBAAECggEADJTNoycS3NdkJ1dqJr+XSCv1nUL3NMn68TWx3SvxWlK4 +gYzmU40OgrKmMgXBOcBjui+XEtoNJhrB463YxQROLf30wr0H5AnEYjgxKHsFhd+5 ++QdkZeUXMD0zX1nlpLoHaOSCDziEGQ2ntXHa5H5D1sPslYRIK3AaCA7kKItAOukO +gaRxDh1N6/u5XZX1hh9Poctydo5gHu6uAZ7y8QzRB3PDjhgeXB6pL/b2VJOnyw2d +EAgDZ04bIEPhLfxNSJR1wlCXcNc4LUh3xGy38Gyvfd2hzWrQRM46RmvtTUKpY+7a +DR+v8+pJDQm4zsol9ncaJKgQtIJgzpqj3TfltnCFsQKBgQDnPdST5JNY25w/wai1 +lek0qDewkD6dg/8P5mASOwN/A/ay+9lgpHncjreYLrQRtvxWYfm7wNagyYmhN4lF +IH8skqoWbDiz4qxUNuR5TAHx+g2hpXK/wKbCcafqYV6L7Lc+5/x7/k7mqVMkf/D+ +7XmQhFVGItJMjVG4W5UOfGGbRQKBgQDP7nqwt5Q+qSBNFOMg2433PVve5lyBpTkH +Fca8GDd5aa7aEbdZwDOlKHjgXu/Heg8uTVHZGf725c9j6Nsda9vPlfAdeu5VxdJs +5kqZro0/AoW+vpkmtpk7M/Ot5Y7R+5IRSlBCb17RzSm2pqmJLacefbvYa09QjcyC +LmAM2o75kwKBgQDb6znYzXI09+dJ22wQBlqb8b/E8+oY9AgHnxmPPQC+M47T+iFq +gAJFeJWy7ffjQRwLK3LO1T9J+2IhKSgrzhQk1/dbC+GBcvphvTLdCSRwdVexfB/9 +rcLq+hywE5pPiPldolPFuL5hMHgaJnOUf1U11CUlZsiKdXxa0P6ZoEFT4QKBgQCB +ReoffjL7dhiv86F2JyovIYXBogS3UaqP3hkNjhzHLk5YI5WThixVrUDhdgSrRxaz +Gb0eNcxPYfc7TWUU+J7Tg4uiOHB/ARtfOxn8TApit0XBniwHZpUDurvwTH0rzbU1 +bLdTZnxUAbLCbQGQWMLC8TbdSXIpSc9wzDZJJ4SmYwKBgD5bunZbmukXYG6ACplx +Q3VRBTU82o0QHVf7ZdYR1kXrO9MdxAS8EF18EODGzfC6p5IlZZZ721RCmZQ7LRsS +fcYWVx1ujSuluGGStdvXVwg3t3j/byJCHUYbxohn0GoDEwMzuoakr1PsEJNewrzs +NZbCsCXwSIqeqCDNNGyEN23m +-----END PRIVATE KEY----- diff --git a/apps/api-gateway/src/user/dto/accept-reject-invitation.dto.ts b/apps/api-gateway/src/user/dto/accept-reject-invitation.dto.ts new file mode 100644 index 000000000..e3d40e7ea --- /dev/null +++ b/apps/api-gateway/src/user/dto/accept-reject-invitation.dto.ts @@ -0,0 +1,28 @@ +import { IsEnum, IsNotEmpty, IsNumber } from 'class-validator'; + +import { ApiProperty } from '@nestjs/swagger'; +import { Invitation } from '@credebl/enum/enum'; +import { Transform } from 'class-transformer'; +import { trim } from '@credebl/common/cast.helper'; + +export class AcceptRejectInvitationDto { + + @ApiProperty({ example: 1 }) + @IsNotEmpty({ message: 'Please provide valid invitationId' }) + @IsNumber() + invitationId: number; + + @ApiProperty({ example: 1 }) + @IsNotEmpty({ message: 'Please provide valid orgId' }) + @IsNumber() + orgId: number; + + @ApiProperty({ + enum: [Invitation.ACCEPTED, Invitation.REJECTED] + }) + @Transform(({ value }) => trim(value)) + @IsNotEmpty({ message: 'Please provide valid status' }) + @IsEnum(Invitation) + status: Invitation.ACCEPTED | Invitation.REJECTED; + +} diff --git a/apps/api-gateway/src/user/dto/add-user.dto.ts b/apps/api-gateway/src/user/dto/add-user.dto.ts new file mode 100644 index 000000000..51a78d2b6 --- /dev/null +++ b/apps/api-gateway/src/user/dto/add-user.dto.ts @@ -0,0 +1,27 @@ +import { trim } from '@credebl/common/cast.helper'; +import { ApiProperty } from '@nestjs/swagger'; +import { Transform } from 'class-transformer'; +import { IsBoolean, IsNotEmpty, IsOptional, IsString} from 'class-validator'; + +export class AddUserDetails { + @ApiProperty({ example: 'Alen' }) + @IsString({ message: 'firstName should be string' }) + @IsOptional() + firstName?: string; + + @ApiProperty({ example: 'Harvey' }) + @IsString({ message: 'lastName should be string' }) + @IsOptional() + lastName?: string; + + @ApiProperty() + @Transform(({ value }) => trim(value)) + @IsNotEmpty({ message: 'Password is required.' }) + @IsOptional() + password?: string; + + @ApiProperty({ example: 'false' }) + @IsOptional() + @IsBoolean({ message: 'isPasskey should be boolean' }) + isPasskey?: boolean; +} diff --git a/apps/api-gateway/src/user/dto/create-user.dto.ts b/apps/api-gateway/src/user/dto/create-user.dto.ts new file mode 100644 index 000000000..16c097e9c --- /dev/null +++ b/apps/api-gateway/src/user/dto/create-user.dto.ts @@ -0,0 +1,15 @@ +import { IsEmail, IsNotEmpty, MaxLength } from 'class-validator'; +import { toLowerCase, trim } from '@credebl/common/cast.helper'; + +import { ApiProperty } from '@nestjs/swagger'; +import { Transform } from 'class-transformer'; + +export class UserEmailVerificationDto { + @ApiProperty() + @Transform(({ value }) => trim(value)) + @Transform(({ value }) => toLowerCase(value)) + @IsNotEmpty({ message: 'Email is required.' }) + @MaxLength(256, { message: 'Email must be at most 256 character.' }) + @IsEmail() + email: string; +} diff --git a/apps/api-gateway/src/user/dto/email-verify.dto.ts b/apps/api-gateway/src/user/dto/email-verify.dto.ts new file mode 100644 index 000000000..23c4acd5a --- /dev/null +++ b/apps/api-gateway/src/user/dto/email-verify.dto.ts @@ -0,0 +1,21 @@ +import { IsEmail, IsNotEmpty, MaxLength } from 'class-validator'; +import { toLowerCase, trim } from '@credebl/common/cast.helper'; + +import { ApiProperty } from '@nestjs/swagger'; +import { Transform } from 'class-transformer'; + + +export class EmailVerificationDto { + @ApiProperty() + @Transform(({ value }) => trim(value)) + @Transform(({ value }) => toLowerCase(value)) + @IsNotEmpty({ message: 'Email is required.' }) + @MaxLength(256, { message: 'Email must be at most 256 character.' }) + @IsEmail() + email: string; + + @ApiProperty() + @Transform(({ value }) => trim(value)) + @IsNotEmpty({ message: 'Verification code is required.' }) + verificationCode: string; +} diff --git a/apps/api-gateway/src/user/dto/get-all-invitations.dto.ts b/apps/api-gateway/src/user/dto/get-all-invitations.dto.ts new file mode 100644 index 000000000..42e50d3d7 --- /dev/null +++ b/apps/api-gateway/src/user/dto/get-all-invitations.dto.ts @@ -0,0 +1,32 @@ +import { IsOptional, IsString } from 'class-validator'; +import { Transform, Type } from 'class-transformer'; +import { toNumber, trim } from '@credebl/common/cast.helper'; + +import { ApiProperty } from '@nestjs/swagger'; +import { Invitation } from '@credebl/enum/enum'; + +export class GetAllInvitationsDto { + @ApiProperty({ required: false }) + @IsOptional() + @Type(() => Number) + @Transform(({ value }) => toNumber(value)) + pageNumber = 1; + + @ApiProperty({ required: false }) + @IsOptional() + @Type(() => String) + @Transform(({ value }) => trim(value)) + search = ''; + + @ApiProperty({ required: false }) + @IsOptional() + @Type(() => Number) + @Transform(({ value }) => toNumber(value)) + pageSize = 8; + + @ApiProperty({ required: false }) + @IsOptional() + @IsString() + status = Invitation.PENDING; + +} diff --git a/apps/api-gateway/src/user/dto/get-all-users.dto.ts b/apps/api-gateway/src/user/dto/get-all-users.dto.ts new file mode 100644 index 000000000..9130172e5 --- /dev/null +++ b/apps/api-gateway/src/user/dto/get-all-users.dto.ts @@ -0,0 +1,26 @@ +import { Transform, Type } from 'class-transformer'; +import { toNumber, trim } from '@credebl/common/cast.helper'; + +import { ApiProperty } from '@nestjs/swagger'; +import { IsOptional } from 'class-validator'; + +export class GetAllUsersDto { + @ApiProperty({ required: false }) + @IsOptional() + @Type(() => Number) + @Transform(({ value }) => toNumber(value)) + pageNumber = 1; + + @ApiProperty({ required: false }) + @IsOptional() + @Type(() => String) + @Transform(({ value }) => trim(value)) + search = ''; + + @ApiProperty({ required: false }) + @IsOptional() + @Type(() => Number) + @Transform(({ value }) => toNumber(value)) + pageSize = 10; + +} diff --git a/apps/api-gateway/src/user/dto/login-user.dto.ts b/apps/api-gateway/src/user/dto/login-user.dto.ts new file mode 100644 index 000000000..92d9f2350 --- /dev/null +++ b/apps/api-gateway/src/user/dto/login-user.dto.ts @@ -0,0 +1,51 @@ +import { trim } from '@credebl/common/cast.helper'; +import { ApiProperty } from '@nestjs/swagger'; +import { Transform } from 'class-transformer'; +import { IsBoolean, IsEmail, IsNotEmpty, IsOptional, IsString, MaxLength, MinLength } from 'class-validator'; + + +export class LoginUserDto { + @ApiProperty({ example: 'awqx@getnada.com' }) + @IsEmail() + @IsNotEmpty({ message: 'Please provide valid email' }) + @IsString({ message: 'email should be string' }) + email: string; + + @ApiProperty() + @Transform(({ value }) => trim(value)) + @IsNotEmpty({ message: 'Password is required.' }) + @MinLength(8, { message: 'Password must be at least 8 characters.' }) + @MaxLength(50, { message: 'Password must be at most 50 characters.' }) + @IsOptional() + password?: string; + + @ApiProperty({ example: 'false' }) + @IsOptional() + @IsBoolean({ message: 'isPasskey should be boolean' }) + isPasskey: boolean; +} + +export class AddUserDetails { + @ApiProperty({ example: 'Alen' }) + @IsString({ message: 'firstName should be string' }) + @IsOptional() + firstName?: string; + + @ApiProperty({ example: 'Harvey' }) + @IsString({ message: 'lastName should be string' }) + @IsOptional() + lastName?: string; + + @ApiProperty() + @Transform(({ value }) => trim(value)) + @IsNotEmpty({ message: 'Password is required.' }) + @MinLength(8, { message: 'Password must be at least 8 characters.' }) + @MaxLength(50, { message: 'Password must be at most 50 characters.' }) + @IsOptional() + password?: string; + + @ApiProperty({ example: 'false' }) + @IsOptional() + @IsBoolean({ message: 'isPasskey should be boolean' }) + isPasskey?: boolean; +} diff --git a/apps/api-gateway/src/user/interfaces/index.ts b/apps/api-gateway/src/user/interfaces/index.ts new file mode 100644 index 000000000..97ee10964 --- /dev/null +++ b/apps/api-gateway/src/user/interfaces/index.ts @@ -0,0 +1,23 @@ +export interface IUserRequestInterface { + userId: number; + email: string; + orgId: number; + agentEndPoint?: string; + apiKey?: string; + orgName?: string; + selectedOrg: ISelectedOrgInterface; +} + +export interface ISelectedOrgInterface { + id: number; + userId: number; + orgRoleId: number; + orgId: number; + orgRole: object; + organisation: IOrganizationInterface; +} + +export interface IOrganizationInterface { + name: string; + description: string; +} diff --git a/apps/api-gateway/src/user/user.controller.ts b/apps/api-gateway/src/user/user.controller.ts new file mode 100644 index 000000000..4daa97498 --- /dev/null +++ b/apps/api-gateway/src/user/user.controller.ts @@ -0,0 +1,268 @@ +import { Controller, Post, Body, Param } from '@nestjs/common'; +import { UserService } from './user.service'; +import { UserEmailVerificationDto } from './dto/create-user.dto'; +import { + ApiBearerAuth, + ApiBody, + ApiForbiddenResponse, + ApiOperation, + ApiResponse, + ApiTags, + ApiUnauthorizedResponse +} from '@nestjs/swagger'; +import { ApiResponseDto } from '../dtos/apiResponse.dto'; +import { UnauthorizedErrorDto } from '../dtos/unauthorized-error.dto'; +import { ForbiddenErrorDto } from '../dtos/forbidden-error.dto'; +import { Res } from '@nestjs/common'; +import { Response } from 'express'; +import { HttpStatus } from '@nestjs/common'; +import { CommonService } from '@credebl/common'; +import IResponseType from '@credebl/common/interfaces/response.interface'; +import { BadRequestException } from '@nestjs/common'; +import { AuthTokenResponse } from '../authz/dtos/auth-token-res.dto'; +import { LoginUserDto } from './dto/login-user.dto'; +import { UnauthorizedException } from '@nestjs/common'; +import { ResponseMessages } from '@credebl/common/response-messages'; +import { EmailVerificationDto } from './dto/email-verify.dto'; +import { Get } from '@nestjs/common'; +import { Query } from '@nestjs/common'; +import { user } from '@prisma/client'; +import { UseGuards } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; +import { User } from '../authz/decorators/user.decorator'; +import { AcceptRejectInvitationDto } from './dto/accept-reject-invitation.dto'; +import { Invitation } from '@credebl/enum/enum'; +import { OrgRolesGuard } from '../authz/guards/org-roles.guard'; +import { Roles } from '../authz/decorators/roles.decorator'; +import { OrgRoles } from 'libs/org-roles/enums'; +import { IUserRequestInterface } from './interfaces'; +import { GetAllInvitationsDto } from './dto/get-all-invitations.dto'; +import { GetAllUsersDto } from './dto/get-all-users.dto'; +import { AddUserDetails } from './dto/add-user.dto'; + +@Controller('users') +@ApiTags('users') +@ApiUnauthorizedResponse({ status: 401, description: 'Unauthorized', type: UnauthorizedErrorDto }) +@ApiForbiddenResponse({ status: 403, description: 'Forbidden', type: ForbiddenErrorDto }) +export class UserController { + constructor(private readonly userService: UserService, private readonly commonService: CommonService) { } + + /** + * + * @param email + * @param res + * @returns Email sent success + */ + @Post('/send-mail') + @ApiResponse({ status: 201, description: 'Success', type: ApiResponseDto }) + @ApiOperation({ summary: 'Send verification email', description: 'Send verification email to new user' }) + async create(@Body() userEmailVerificationDto: UserEmailVerificationDto, @Res() res: Response): Promise { + await this.userService.sendVerificationMail(userEmailVerificationDto); + const finalResponse: IResponseType = { + statusCode: HttpStatus.OK, + message: ResponseMessages.user.success.sendVerificationCode + }; + return res.status(HttpStatus.CREATED).json(finalResponse); + } + + /** + * + * @param user + * @param orgId + * @param res + * @returns Users list of organization + */ + @Get() + @Roles(OrgRoles.OWNER, OrgRoles.ADMIN, OrgRoles.HOLDER, OrgRoles.ISSUER, OrgRoles.SUPER_ADMIN, OrgRoles.SUPER_ADMIN, OrgRoles.MEMBER) + @ApiBearerAuth() + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + @ApiResponse({ status: 200, description: 'Success', type: ApiResponseDto }) + @ApiOperation({ summary: 'Get organization users list', description: 'Get organization users list.' }) + async get(@User() user: IUserRequestInterface, @Query() getAllUsersDto: GetAllUsersDto, @Query('orgId') orgId: number, @Res() res: Response): Promise { + + const org = user.selectedOrg?.orgId; + const users = await this.userService.get(org, getAllUsersDto); + const finalResponse: IResponseType = { + statusCode: HttpStatus.OK, + message: ResponseMessages.user.success.fetchUsers, + data: users.response + }; + + return res.status(HttpStatus.OK).json(finalResponse); + } + + + /** + * + * @param query + * @param res + * @returns User email verified + */ + @Get('/verify') + @ApiOperation({ summary: 'Verify new users email', description: 'Email verification for new users' }) + async verifyEmail(@Query() query: EmailVerificationDto, @Res() res: Response): Promise { + await this.userService.verifyEmail(query); + const finalResponse: IResponseType = { + statusCode: HttpStatus.OK, + message: ResponseMessages.user.success.emaiVerified + }; + + return res.status(HttpStatus.OK).json(finalResponse); + + } + + /** + * + * @param loginUserDto + * @param res + * @returns User access token details + */ + @Post('/login') + @ApiOperation({ + summary: 'Login API for web portal', + description: 'Password should be AES encrypted.' + }) + @ApiResponse({ status: 200, description: 'Success', type: AuthTokenResponse }) + @ApiBody({ type: LoginUserDto }) + async login(@Body() loginUserDto: LoginUserDto, @Res() res: Response): Promise { + + if (loginUserDto.email) { + let decryptedPassword; + if (loginUserDto.password) { + decryptedPassword = this.commonService.decryptPassword(loginUserDto.password); + } + const userData = await this.userService.login(loginUserDto.email, decryptedPassword, loginUserDto.isPasskey); + const finalResponse: IResponseType = { + statusCode: HttpStatus.OK, + message: ResponseMessages.user.success.login, + data: userData.response + }; + + return res.status(HttpStatus.OK).json(finalResponse); + } else { + throw new UnauthorizedException(`Please provide valid credentials`); + } + } + + @Get('profile') + @ApiOperation({ + summary: 'Fetch login user details', + description: 'Fetch login user details' + }) + @UseGuards(AuthGuard('jwt')) + @ApiBearerAuth() + async getProfile(@User() reqUser: user, @Res() res: Response): Promise { + + const userData = await this.userService.getProfile(reqUser.id); + + const finalResponse: IResponseType = { + statusCode: HttpStatus.OK, + message: ResponseMessages.user.success.fetchProfile, + data: userData.response + }; + + return res.status(HttpStatus.OK).json(finalResponse); + + } + + @Get('invitations') + @ApiOperation({ + summary: 'organization invitations', + description: 'Fetch organization invitations' + }) + @UseGuards(AuthGuard('jwt')) + @ApiBearerAuth() + async invitations(@User() reqUser: user, @Query() getAllInvitationsDto: GetAllInvitationsDto, @Res() res: Response): Promise { + + if (!Object.values(Invitation).includes(getAllInvitationsDto.status)) { + throw new BadRequestException(ResponseMessages.user.error.invalidInvitationStatus); + } + + const invitations = await this.userService.invitations(reqUser.id, getAllInvitationsDto.status, getAllInvitationsDto); + + const finalResponse: IResponseType = { + statusCode: HttpStatus.OK, + message: ResponseMessages.user.success.fetchInvitations, + data: invitations.response + }; + + return res.status(HttpStatus.OK).json(finalResponse); + + } + + + /** + * + * @param acceptRejectInvitation + * @param reqUser + * @param res + * @returns Organization invitation status + */ + @Post('invitations') + @ApiOperation({ + summary: 'accept/reject organization invitation', + description: 'Accept or Reject organization invitations' + }) + @UseGuards(AuthGuard('jwt')) + @ApiBearerAuth() + async acceptRejectInvitaion(@Body() acceptRejectInvitation: AcceptRejectInvitationDto, @User() reqUser: user, @Res() res: Response): Promise { + const invitationRes = await this.userService.acceptRejectInvitaion(acceptRejectInvitation, reqUser.id); + + const finalResponse: IResponseType = { + statusCode: HttpStatus.OK, + message: invitationRes.response + }; + + return res.status(HttpStatus.OK).json(finalResponse); + + } + + /** + * + * @param email + * @param res + * @returns User email check + */ + @Get('/check-user/:email') + @ApiOperation({ summary: 'Check user exist', description: 'check user existence' }) + async checkUserExist(@Param('email') email: string, @Res() res: Response): Promise { + const userDetails = await this.userService.checkUserExist(email); + + const finalResponse: IResponseType = { + statusCode: HttpStatus.OK, + message: ResponseMessages.user.success.checkEmail, + data: userDetails.response + }; + + return res.status(HttpStatus.OK).json(finalResponse); + + } + + /** + * + * @param email + * @param userInfo + * @param res + * @returns Add new user + */ + @Post('/add/:email') + @ApiOperation({ summary: 'Add user information', description: 'Add user information' }) + async addUserDetailsInKeyCloak(@Body() userInfo: AddUserDetails, @Param('email') email: string, @Res() res: Response): Promise { + const decryptedPassword = this.commonService.decryptPassword(userInfo.password); + if (8 <= decryptedPassword.length && 50 >= decryptedPassword.length) { + this.commonService.passwordValidation(decryptedPassword); + userInfo.password = decryptedPassword; + const userDetails = await this.userService.addUserDetailsInKeyCloak(email, userInfo); + const finalResponse: IResponseType = { + statusCode: HttpStatus.CREATED, + message: ResponseMessages.user.success.create, + data: userDetails.response + }; + return res.status(HttpStatus.OK).json(finalResponse); + + } else { + throw new BadRequestException('Password name must be between 8 to 50 Characters'); + } + + } +} \ No newline at end of file diff --git a/apps/api-gateway/src/user/user.module.ts b/apps/api-gateway/src/user/user.module.ts new file mode 100644 index 000000000..7d494c009 --- /dev/null +++ b/apps/api-gateway/src/user/user.module.ts @@ -0,0 +1,26 @@ +import { 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 { UserController } from './user.controller'; +import { UserService } from './user.service'; + +@Module({ + imports: [ + HttpModule, + ConfigModule.forRoot(), + ClientsModule.register([ + { + name: 'NATS_CLIENT', + transport: Transport.NATS, + options: { + servers: [`${process.env.NATS_URL}`] + } + } + ]) + ], + controllers: [UserController], + providers: [UserService, CommonService] +}) +export class UserModule {} diff --git a/apps/api-gateway/src/user/user.service.ts b/apps/api-gateway/src/user/user.service.ts new file mode 100644 index 000000000..b3e363337 --- /dev/null +++ b/apps/api-gateway/src/user/user.service.ts @@ -0,0 +1,96 @@ +import { Inject } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; +import { ClientProxy, RpcException } from '@nestjs/microservices'; +import { BaseService } from 'libs/service/base.service'; +import { AcceptRejectInvitationDto } from './dto/accept-reject-invitation.dto'; +import { UserEmailVerificationDto } from './dto/create-user.dto'; +import { EmailVerificationDto } from './dto/email-verify.dto'; +import { GetAllInvitationsDto } from './dto/get-all-invitations.dto'; +import { AddUserDetails } from './dto/login-user.dto'; +import { GetAllUsersDto } from './dto/get-all-users.dto'; + +@Injectable() +export class UserService extends BaseService { + constructor(@Inject('NATS_CLIENT') private readonly serviceProxy: ClientProxy) { + super('User Service'); + } + + async sendVerificationMail(userEmailVerificationDto: UserEmailVerificationDto): Promise { + try { + const payload = { userEmailVerificationDto }; + return this.sendNats(this.serviceProxy, 'send-verification-mail', payload); + } catch (error) { + throw new RpcException(error.response); + } + } + + async login(email: string, password?: string, isPasskey = false): Promise<{ response: object }> { + try { + const payload = { email, password, isPasskey }; + return this.sendNats(this.serviceProxy, 'user-holder-login', payload); + } catch (error) { + throw new RpcException(error.response); + } + } + + async verifyEmail(param: EmailVerificationDto): Promise { + try { + const payload = { param }; + return this.sendNats(this.serviceProxy, 'user-email-verification', payload); + } catch (error) { + throw new RpcException(error.response); + } + } + + async getProfile(id: number): Promise<{ response: object }> { + const payload = { id }; + try { + return this.sendNats(this.serviceProxy, 'get-user-profile', payload); + } catch (error) { + this.logger.error(`Error in get user:${JSON.stringify(error)}`); + } + } + + async findUserByKeycloakId(id: string): Promise<{ response: object }> { + const payload = { id }; + + try { + return this.sendNats(this.serviceProxy, 'get-user-by-keycloak-id', payload); + } catch (error) { + this.logger.error(`Error in get user:${JSON.stringify(error)}`); + } + } + + async invitations(id: number, status: string, getAllInvitationsDto: GetAllInvitationsDto): Promise<{ response: object }> { + const {pageNumber, pageSize, search} = getAllInvitationsDto; + const payload = { id, status, pageNumber, pageSize, search }; + return this.sendNats(this.serviceProxy, 'get-org-invitations', payload); + } + + async acceptRejectInvitaion( + acceptRejectInvitation: AcceptRejectInvitationDto, + userId: number + ): Promise<{ response: string }> { + const payload = { acceptRejectInvitation, userId }; + return this.sendNats(this.serviceProxy, 'accept-reject-invitations', payload); + } + + async get( + orgId: number, + getAllUsersDto: GetAllUsersDto + ): Promise<{ response: object }> { + const {pageNumber, pageSize, search} = getAllUsersDto; + const payload = { orgId, pageNumber, pageSize, search }; + return this.sendNats(this.serviceProxy, 'fetch-organization-users', payload); + } + + async checkUserExist(userEmail: string): Promise<{ response: string }> { + const payload = { userEmail }; + return this.sendNats(this.serviceProxy, 'check-user-exist', payload); + } + + async addUserDetailsInKeyCloak(userEmail: string, userInfo:AddUserDetails): Promise<{ response: string }> { + const payload = { userEmail, userInfo }; + return this.sendNats(this.serviceProxy, 'add-user', 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 new file mode 100644 index 000000000..306bcd464 --- /dev/null +++ b/apps/api-gateway/src/verification/dto/request-proof.dto.ts @@ -0,0 +1,48 @@ +import { IsArray, IsNotEmpty, IsNumber, IsObject, IsOptional, IsString, MaxLength } from 'class-validator'; +import { toLowerCase, trim } from '@credebl/common/cast.helper'; +import { ApiProperty } from '@nestjs/swagger'; +import { Transform } from 'class-transformer'; +import { IProofRequestAttribute } from '../interfaces/verification.interface'; + +export class RequestProof { + @ApiProperty() + @Transform(({ value }) => trim(value)) + @Transform(({ value }) => toLowerCase(value)) + @IsNotEmpty({ message: 'connectionId is required.' }) + @MaxLength(36, { message: 'connectionId must be at most 36 character.' }) + connectionId: string; + + @ApiProperty({ + 'example': [ + { + attributeName: 'attributeName', + condition: '>=', + value: 'predicates', + credDefId: '' + } + ] + }) + @IsArray({ message: 'attributes must be in array' }) + @IsObject({ each: true }) + @IsNotEmpty({ message: 'please provide valid attributes' }) + attributes: IProofRequestAttribute[]; + + @ApiProperty() + @IsOptional() + comment: string; + + @ApiProperty() + @IsNumber() + @IsNotEmpty({ message: 'please provide orgId' }) + orgId: number; + + @IsString({ message: 'autoAcceptProof must be in string' }) + @IsNotEmpty({ message: 'please provide valid autoAcceptProof' }) + @IsOptional() + autoAcceptProof: string; + + @IsString({ message: 'protocolVersion must be in string' }) + @IsNotEmpty({ message: 'please provide valid protocolVersion' }) + @IsOptional() + protocolVersion: string; +} diff --git a/apps/api-gateway/src/verification/dto/webhook-proof.dto.ts b/apps/api-gateway/src/verification/dto/webhook-proof.dto.ts new file mode 100644 index 000000000..4eb3c10a6 --- /dev/null +++ b/apps/api-gateway/src/verification/dto/webhook-proof.dto.ts @@ -0,0 +1,55 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsOptional } from "class-validator"; + +interface IWebhookPresentationProof { + threadId: string; + state: string; + connectionId +} + +export class WebhookPresentationProof { + + @ApiProperty() + @IsOptional() + metadata: object; + + @ApiProperty() + @IsOptional() + _tags: IWebhookPresentationProof; + + @ApiProperty() + @IsOptional() + id: string; + + @ApiProperty() + @IsOptional() + createdAt: string; + + @ApiProperty() + @IsOptional() + protocolVersion: string; + + @ApiProperty() + @IsOptional() + state: string; + + @ApiProperty() + @IsOptional() + connectionId: string; + + @ApiProperty() + @IsOptional() + threadId: string; + + @ApiProperty() + @IsOptional() + autoAcceptProof: string; + + @ApiProperty() + @IsOptional() + updatedAt: string; + + @ApiProperty() + @IsOptional() + isVerified: boolean; +} \ 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 new file mode 100644 index 000000000..978f1a039 --- /dev/null +++ b/apps/api-gateway/src/verification/interfaces/verification.interface.ts @@ -0,0 +1,7 @@ +export interface IProofRequestAttribute { + attributeName: string; + condition?: string; + value?: string; + credDefId: string; + credentialName: 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 new file mode 100644 index 000000000..6d1e7fa8a --- /dev/null +++ b/apps/api-gateway/src/verification/verification.controller.ts @@ -0,0 +1,209 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable no-param-reassign */ +/* eslint-disable camelcase */ +import { + ApiBearerAuth, + ApiTags, + ApiOperation, + ApiResponse, + ApiUnauthorizedResponse, + ApiForbiddenResponse, + ApiBody, + ApiQuery, + ApiExcludeEndpoint +} from '@nestjs/swagger'; +import { Controller, Logger, Post, Body, Get, Query, HttpStatus, Res, UseGuards, Param } from '@nestjs/common'; +import { ApiResponseDto } from '../dtos/apiResponse.dto'; +import { UnauthorizedErrorDto } from '../dtos/unauthorized-error.dto'; +import { ForbiddenErrorDto } from '../dtos/forbidden-error.dto'; +import { RequestProof } from './dto/request-proof.dto'; +import { GetUser } from '../authz/decorators/get-user.decorator'; +import { VerificationService } from './verification.service'; +import IResponseType from '@credebl/common/interfaces/response.interface'; +import { Response } from 'express'; +import { ResponseMessages } from '@credebl/common/response-messages'; +import { IUserRequest } from '@credebl/user-request/user-request.interface'; +import { Roles } from '../authz/decorators/roles.decorator'; +import { OrgRoles } from 'libs/org-roles/enums'; +import { AuthGuard } from '@nestjs/passport'; +import { OrgRolesGuard } from '../authz/guards/org-roles.guard'; +import { WebhookPresentationProof } from './dto/webhook-proof.dto'; + +@ApiBearerAuth() +@Controller() +export class VerificationController { + constructor(private readonly verificationService: VerificationService) { } + + private readonly logger = new Logger('VerificationController'); + + /** + * Get all proof presentations + * @param user + * @param orgId + * @returns Get all proof presentation + */ + @Get('/proofs') + @ApiTags('verifications') + @ApiOperation({ + summary: `Get all proof-presentation`, + description: `Get all proof-presentation` + }) + @ApiResponse({ status: 201, description: 'Success', type: ApiResponseDto }) + @ApiUnauthorizedResponse({ status: 401, description: 'Unauthorized', type: UnauthorizedErrorDto }) + @ApiForbiddenResponse({ status: 403, description: 'Forbidden', type: ForbiddenErrorDto }) + @ApiQuery( + { name: 'orgId', required: true } + ) + @ApiQuery( + { name: 'threadId', required: false } + ) + @Roles(OrgRoles.OWNER, OrgRoles.ADMIN, OrgRoles.VERIFIER) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + async getProofPresentations( + @Res() res: Response, + @GetUser() user: IUserRequest, + @Query('orgId') orgId: number, + @Query('threadId') threadId: string + ): Promise { + const proofPresentationDetails = await this.verificationService.getProofPresentations(orgId, threadId, user); + const finalResponse: IResponseType = { + statusCode: HttpStatus.OK, + message: ResponseMessages.verification.success.fetch, + data: proofPresentationDetails.response + }; + return res.status(HttpStatus.OK).json(finalResponse); + } + + /** + * Get proof presentation by id + * @param user + * @param id + * @param orgId + * @returns Get proof presentation details + */ + @Get('/proofs/:id') + @ApiTags('verifications') + @ApiOperation({ + summary: `Get proof-presentation by Id`, + description: `Get proof-presentation by Id` + }) + @ApiResponse({ status: 201, description: 'Success', type: ApiResponseDto }) + @ApiUnauthorizedResponse({ status: 401, description: 'Unauthorized', type: UnauthorizedErrorDto }) + @ApiForbiddenResponse({ status: 403, description: 'Forbidden', type: ForbiddenErrorDto }) + @ApiQuery( + { name: 'orgId', required: true } + ) + @Roles(OrgRoles.OWNER, OrgRoles.ADMIN, OrgRoles.VERIFIER) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + async getProofPresentationById( + @Res() res: Response, + @GetUser() user: IUserRequest, + @Param('id') id: string, + @Query('orgId') orgId: number + ): Promise { + const getProofPresentationById = await this.verificationService.getProofPresentationById(id, orgId, user); + const finalResponse: IResponseType = { + statusCode: HttpStatus.OK, + message: ResponseMessages.verification.success.fetch, + data: getProofPresentationById.response + }; + return res.status(HttpStatus.OK).json(finalResponse); + } + + /** + * Request proof presentation + * @param user + * @param requestProof + * @returns Get requested proof presentation details + */ + @Post('/proofs/request-proof') + @ApiTags('verifications') + @ApiOperation({ + summary: `Sends a proof request`, + description: `Sends a proof request` + }) + @ApiResponse({ status: 201, description: 'Success', type: ApiResponseDto }) + @ApiUnauthorizedResponse({ status: 401, description: 'Unauthorized', type: UnauthorizedErrorDto }) + @ApiForbiddenResponse({ status: 403, description: 'Forbidden', type: ForbiddenErrorDto }) + @ApiBody({ type: RequestProof }) + @Roles(OrgRoles.OWNER, OrgRoles.ADMIN, OrgRoles.VERIFIER) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + async sendPresentationRequest( + @Res() res: Response, + @GetUser() user: IUserRequest, + @Body() requestProof: RequestProof + ): Promise { + const sendProofRequest = await this.verificationService.sendProofRequest(requestProof, user); + const finalResponse: IResponseType = { + statusCode: HttpStatus.CREATED, + message: ResponseMessages.verification.success.fetch, + data: sendProofRequest.response + }; + return res.status(HttpStatus.CREATED).json(finalResponse); + } + + /** + * Verify proof presentation + * @param user + * @param id + * @param orgId + * @returns Get verified proof presentation details + */ + @Post('proofs/verify-presentation') + @ApiTags('verifications') + @ApiOperation({ + summary: `Verify presentation`, + description: `Verify presentation` + }) + @ApiResponse({ status: 201, description: 'Success', type: ApiResponseDto }) + @ApiUnauthorizedResponse({ status: 401, description: 'Unauthorized', type: UnauthorizedErrorDto }) + @ApiForbiddenResponse({ status: 403, description: 'Forbidden', type: ForbiddenErrorDto }) + @ApiQuery( + { name: 'id', required: true } + ) + @ApiQuery( + { name: 'orgId', required: true } + ) + @Roles(OrgRoles.OWNER, OrgRoles.ADMIN, OrgRoles.VERIFIER) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + async verifyPresentation( + @Res() res: Response, + @GetUser() user: IUserRequest, + @Query('id') id: string, + @Query('orgId') orgId: number + ): Promise { + const verifyPresentation = await this.verificationService.verifyPresentation(id, orgId, user); + const finalResponse: IResponseType = { + statusCode: HttpStatus.CREATED, + message: ResponseMessages.verification.success.verified, + data: verifyPresentation.response + }; + return res.status(HttpStatus.CREATED).json(finalResponse); + } + + @Post('wh/:id/proofs') + @ApiTags('verifications') + @ApiOperation({ + summary: `Webhook proof presentation`, + description: `Webhook proof presentation` + }) + @ApiExcludeEndpoint() + @ApiResponse({ status: 201, description: 'Success', type: ApiResponseDto }) + @ApiUnauthorizedResponse({ status: 401, description: 'Unauthorized', type: UnauthorizedErrorDto }) + @ApiForbiddenResponse({ status: 403, description: 'Forbidden', type: ForbiddenErrorDto }) + async webhookProofPresentation( + @Param('id') id: string, + @Body() proofPresentationPayload: WebhookPresentationProof, + @Res() res: Response + ): Promise { + + const webhookProofPresentation = await this.verificationService.webhookProofPresentation(id, proofPresentationPayload); + const finalResponse: IResponseType = { + statusCode: HttpStatus.CREATED, + message: ResponseMessages.verification.success.fetch, + data: webhookProofPresentation.response + }; + return res.status(HttpStatus.CREATED).json(finalResponse); + } +} + diff --git a/apps/api-gateway/src/verification/verification.module.ts b/apps/api-gateway/src/verification/verification.module.ts new file mode 100644 index 000000000..7fa2c1353 --- /dev/null +++ b/apps/api-gateway/src/verification/verification.module.ts @@ -0,0 +1,23 @@ +import { ClientsModule } from '@nestjs/microservices'; + +import { ConfigModule } from '@nestjs/config'; +import { Module } from '@nestjs/common'; +import { VerificationController } from './verification.controller'; +import { VerificationService } from './verification.service'; +import { commonNatsOptions } from 'libs/service/nats.options'; + +@Module({ + imports: [ + ConfigModule.forRoot(), + ClientsModule.register([ + { + name: 'NATS_CLIENT', + ...commonNatsOptions('VERIFICATION_SERVICE:REQUESTER') + } + ]) + + ], + controllers: [VerificationController], + providers: [VerificationService] +}) +export class VerificationModule { } diff --git a/apps/api-gateway/src/verification/verification.service.ts b/apps/api-gateway/src/verification/verification.service.ts new file mode 100644 index 000000000..3dcc8ec8a --- /dev/null +++ b/apps/api-gateway/src/verification/verification.service.ts @@ -0,0 +1,67 @@ +import { Injectable, Inject } from '@nestjs/common'; +import { ClientProxy } from '@nestjs/microservices'; +import { BaseService } from 'libs/service/base.service'; +import { RequestProof } from './dto/request-proof.dto'; +import { IUserRequest } from '@credebl/user-request/user-request.interface'; +import { WebhookPresentationProof } from './dto/webhook-proof.dto'; + + +@Injectable() +export class VerificationService extends BaseService { + constructor( + @Inject('NATS_CLIENT') private readonly verificationServiceProxy: ClientProxy + ) { + super('VerificationService'); + } + + /** + * Get all proof presentations + * @param orgId + * @param user + * @returns Get all proof presentation + */ + getProofPresentations(orgId: number, threadId: string, user: IUserRequest): Promise<{ response: object }> { + const payload = { user, threadId, orgId }; + return this.sendNats(this.verificationServiceProxy, 'get-proof-presentations', payload); + } + + /** + * Get proof presentation by id + * @param id + * @param orgId + * @param user + * @returns Get proof presentation details + */ + getProofPresentationById(id: string, orgId: number, user: IUserRequest): Promise<{ response: object }> { + const payload = { id, orgId, user }; + return this.sendNats(this.verificationServiceProxy, 'get-proof-presentations-by-id', payload); + } + + /** + * Request proof presentation + * @param requestProof + * @param user + * @returns Get requested proof presentation details + */ + sendProofRequest(requestProof: RequestProof, user: IUserRequest): Promise<{ response: object }> { + const payload = { requestProof, user }; + return this.sendNats(this.verificationServiceProxy, 'send-proof-request', payload); + } + + /** + * Request proof presentation + * @param id + * @param orgId + * @param user + * @returns Get requested proof presentation details + */ + verifyPresentation(id: string, orgId: number, user: IUserRequest): Promise<{ response: object }> { + const payload = { id, orgId, user }; + return this.sendNats(this.verificationServiceProxy, 'verify-presentation', payload); + } + + webhookProofPresentation(id: string, proofPresentationPayload: WebhookPresentationProof): Promise<{ response: object }> { + const payload = { id, proofPresentationPayload }; + return this.sendNats(this.verificationServiceProxy, 'webhook-proof-presentation', payload); + } +} diff --git a/apps/api-gateway/test/app.e2e-spec.ts b/apps/api-gateway/test/app.e2e-spec.ts new file mode 100644 index 000000000..3430c90ea --- /dev/null +++ b/apps/api-gateway/test/app.e2e-spec.ts @@ -0,0 +1,22 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { INestApplication } from '@nestjs/common'; +import * as request from 'supertest'; +import { AppModule } from './../src/app.module'; + +describe('AppController (e2e)', () => { + let app: INestApplication; + + beforeEach(async () => { + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [AppModule] + }).compile(); + + app = moduleFixture.createNestApplication(); + await app.init(); + }); + + it('/ (GET)', () => request(app.getHttpServer()) + .get('/') + .expect(200) + .expect('Hello World!')); +}); diff --git a/apps/api-gateway/test/jest-e2e.json b/apps/api-gateway/test/jest-e2e.json new file mode 100644 index 000000000..e9d912f3e --- /dev/null +++ b/apps/api-gateway/test/jest-e2e.json @@ -0,0 +1,9 @@ +{ + "moduleFileExtensions": ["js", "json", "ts"], + "rootDir": ".", + "testEnvironment": "node", + "testRegex": ".e2e-spec.ts$", + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + } +} diff --git a/apps/api-gateway/tsconfig.app.json b/apps/api-gateway/tsconfig.app.json new file mode 100644 index 000000000..9b222ab12 --- /dev/null +++ b/apps/api-gateway/tsconfig.app.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "declaration": false, + "outDir": "../../dist/apps/api-gateway" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] +} diff --git a/apps/connection/Dockerfile b/apps/connection/Dockerfile new file mode 100644 index 000000000..917542bad --- /dev/null +++ b/apps/connection/Dockerfile @@ -0,0 +1,43 @@ +# Stage 1: Build the application +FROM node:16-alpine as build + +# Set the working directory +WORKDIR /app + +# Copy package.json and package-lock.json +COPY package.json ./ +#COPY package-lock.json ./ + +# Install dependencies +RUN npm i + +# Copy the rest of the application code +COPY . . +CMD ["sh", "-c", "cd libs/prisma-service && npx prisma generate"] + +# Build the user service +RUN npm run build connection + +# Stage 2: Create the final image +FROM node:16-alpine + +# Set the working directory +WORKDIR /app + +# Copy the compiled code from the build stage +COPY --from=build /app/dist/apps/connection/ ./dist/apps/connection/ + +# 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 + +# Install production dependencies for the final image +#RUN npm i --only=production + +# Set the command to run the microservice +CMD ["sh", "-c", "cd libs/prisma-service && npx prisma generate && cd ../.. && node dist/apps/connection/main.js"] + +# docker build -t connection -f apps/connection/Dockerfile . +# docker run -d --env-file .env --name connection docker.io/library/connection +# docker logs -f connection diff --git a/apps/connection/src/connection.controller.ts b/apps/connection/src/connection.controller.ts new file mode 100644 index 000000000..efedddefc --- /dev/null +++ b/apps/connection/src/connection.controller.ts @@ -0,0 +1,53 @@ +import { Controller } from '@nestjs/common'; // Import the common service in the library +import { ConnectionService } from './connection.service'; // Import the common service in connection module +import { MessagePattern } from '@nestjs/microservices'; // Import the nestjs microservices package +import { IConnection, IConnectionInterface, IFetchConnectionById, IFetchConnectionInterface } from './interfaces/connection.interfaces'; + +@Controller() +export class ConnectionController { + constructor(private readonly connectionService: ConnectionService) { } + + /** + * Description: Create out-of-band connection legacy invitation + * @param payload + * @returns Created connection invitation for out-of-band + */ + @MessagePattern({ cmd: 'create-connection' }) + async createLegacyConnectionInvitation(payload: IConnection): Promise { + const { orgId, user, multiUseInvitation, autoAcceptConnection, alias, imageUrl, label } = payload; + return this.connectionService.createLegacyConnectionInvitation(orgId, user, multiUseInvitation, autoAcceptConnection, alias, imageUrl, label); + } + + /** + * Description: Catch connection webhook responses and save details in connection table + * @param payload + * @returns Callback URL for connection and created connections details + */ + @MessagePattern({ cmd: 'webhook-get-connection' }) + async getConnectionWebhook(payload: IConnectionInterface): Promise { + const { createDateTime, lastChangedDateTime, connectionId, state, orgDid, theirLabel, autoAcceptConnection, outOfBandId, orgId } = payload; + return this.connectionService.getConnectionWebhook(createDateTime, lastChangedDateTime, connectionId, state, orgDid, theirLabel, autoAcceptConnection, outOfBandId, orgId); + } + + /** + * Description: Fetch connection url by refernceId. + * @param payload + * @returns Created connection invitation for out-of-band + */ + @MessagePattern({ cmd: 'get-connection-url' }) + async getUrl(payload: { referenceId }): Promise { + return this.connectionService.getUrl(payload.referenceId); + } + + @MessagePattern({ cmd: 'get-all-connections' }) + async getConnections(payload: IFetchConnectionInterface): Promise { + const { user, outOfBandId, alias, state, myDid, theirDid, theirLabel, orgId } = payload; + return this.connectionService.getConnections(user, outOfBandId, alias, state, myDid, theirDid, theirLabel, orgId); + } + + @MessagePattern({ cmd: 'get-all-connections-by-connectionId' }) + async getConnectionsById(payload: IFetchConnectionById): Promise { + const { user, connectionId, orgId } = payload; + return this.connectionService.getConnectionsById(user, connectionId, orgId); + } +} diff --git a/apps/connection/src/connection.module.ts b/apps/connection/src/connection.module.ts new file mode 100644 index 000000000..e9cc372a8 --- /dev/null +++ b/apps/connection/src/connection.module.ts @@ -0,0 +1,26 @@ +import { Logger, Module } from '@nestjs/common'; +import { ConnectionController } from './connection.controller'; +import { ConnectionService } from './connection.service'; +import { ClientsModule, Transport } from '@nestjs/microservices'; +import { CommonModule } from '@credebl/common'; +import { ConnectionRepository } from './connection.repository'; +import { PrismaService } from '@credebl/prisma-service'; + +@Module({ + imports: [ + ClientsModule.register([ + { + name: 'NATS_CLIENT', + transport: Transport.NATS, + options: { + servers: [`${process.env.NATS_URL}`] + } + } + ]), + + CommonModule + ], + controllers: [ConnectionController], + providers: [ConnectionService, ConnectionRepository, PrismaService, Logger] +}) +export class ConnectionModule { } diff --git a/apps/connection/src/connection.repository.ts b/apps/connection/src/connection.repository.ts new file mode 100644 index 000000000..6fa1eeb6a --- /dev/null +++ b/apps/connection/src/connection.repository.ts @@ -0,0 +1,149 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { PrismaService } from '@credebl/prisma-service'; +// eslint-disable-next-line camelcase +import { agent_invitations, connections, org_agents, shortening_url } from '@prisma/client'; +@Injectable() +export class ConnectionRepository { + + constructor( + private readonly prisma: PrismaService, + private readonly logger: Logger + ) { } + + /** + * Description: Get getAgentEndPoint by orgId + * @param connectionId + * @returns Get getAgentEndPoint details + */ + // eslint-disable-next-line camelcase + async getAgentEndPoint(orgId: number): Promise { + try { + + const agentDetails = await this.prisma.org_agents.findFirst({ + where: { + orgId + } + }); + return agentDetails; + + } catch (error) { + this.logger.error(`Error in get getAgentEndPoint: ${error.message} `); + throw error; + } + } + + /** + * Description: Save connection details + * @param connectionInvitation + * @param agentId + * @param orgId + * @returns Get connection details + */ + // eslint-disable-next-line camelcase + async saveAgentConnectionInvitations(connectionInvitation: string, agentId: number, orgId: number): Promise { + try { + + const agentDetails = await this.prisma.agent_invitations.create({ + data: { + orgId, + agentId, + connectionInvitation, + multiUse: true + } + }); + return agentDetails; + + } catch (error) { + this.logger.error(`Error in saveAgentConnectionInvitations: ${error.message} `); + throw error; + } + } + + /** + * Description: Save connection details + * @param connectionInvitation + * @param agentId + * @param orgId + * @returns Get connection details + */ + // eslint-disable-next-line camelcase + async saveConnectionWebhook(createDateTime:string, lastChangedDateTime: string, connectionId: string, state: string, orgDid: string, theirLabel: string, autoAcceptConnection:boolean, outOfBandId: string, orgId: number): Promise { + try { + const agentDetails = await this.prisma.connections.upsert({ + where: { + connectionId + }, + update: { + lastChangedDateTime, + lastChangedBy: orgId, + state, + orgDid, + theirLabel, + autoAcceptConnection, + outOfBandId + }, + create: { + createDateTime, + lastChangedDateTime, + connectionId, + state, + orgDid, + theirLabel, + autoAcceptConnection, + outOfBandId, + orgId + } + }); + return agentDetails; + + } catch (error) { + this.logger.error(`Error in saveConnectionWebhook: ${error.message} `); + throw error; + } + } + + /** + * Description: Save ShorteningUrl details + * @param referenceId + * @param connectionInvitationUrl + * @returns Get storeShorteningUrl details + */ + // eslint-disable-next-line camelcase + async storeShorteningUrl(referenceId: string, connectionInvitationUrl: string): Promise { + try { + + return this.prisma.shortening_url.create({ + data: { + referenceId, + url: connectionInvitationUrl, + type: null + } + }); + + } catch (error) { + this.logger.error(`Error in saveAgentConnectionInvitations: ${error.message} `); + throw error; + } + } + + /** + * Description: Fetch ShorteningUrl details + * @param referenceId + * @returns Get storeShorteningUrl details + */ + // eslint-disable-next-line camelcase + async getShorteningUrl(referenceId: string): Promise { + try { + + return this.prisma.shortening_url.findFirst({ + where: { + referenceId + } + }); + + } catch (error) { + this.logger.error(`Error in getShorteningUrl in connection repository: ${error.message} `); + throw error; + } + } +} \ No newline at end of file diff --git a/apps/connection/src/connection.service.ts b/apps/connection/src/connection.service.ts new file mode 100644 index 000000000..9c508b009 --- /dev/null +++ b/apps/connection/src/connection.service.ts @@ -0,0 +1,303 @@ +import { CommonService } from '@credebl/common'; +import { CommonConstants } from '@credebl/common/common.constant'; +import { + HttpException, + Inject, + Injectable, + Logger, + NotFoundException +} from '@nestjs/common'; +import { + ClientProxy, + RpcException +} from '@nestjs/microservices'; +import { map } from 'rxjs'; +import { + IUserRequestInterface +} from './interfaces/connection.interfaces'; +import { ConnectionRepository } from './connection.repository'; +import { ResponseMessages } from '@credebl/common/response-messages'; +import { v4 as uuid } from 'uuid'; +import { IUserRequest } from '@credebl/user-request/user-request.interface'; +import { OrgAgentType } from '@credebl/enum/enum'; + + +@Injectable() +export class ConnectionService { + + constructor( + private readonly commonService: CommonService, + @Inject('NATS_CLIENT') private readonly connectionServiceProxy: ClientProxy, + private readonly connectionRepository: ConnectionRepository, + private readonly logger: Logger + ) { } + + /** + * Description: create connection legacy invitation + * @param orgId + * @param user + * @returns Connection legacy invitation URL + */ + async createLegacyConnectionInvitation( + orgId: number, user: IUserRequestInterface, multiUseInvitation: boolean, autoAcceptConnection: boolean, alias: string, imageUrl: string, label: string + ): Promise { + try { + const agentDetails = await this.connectionRepository.getAgentEndPoint(orgId); + const { agentEndPoint, id } = 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: imageUrl || undefined, + label: label || undefined + }; + + const url = await this.getAgentUrl(agentDetails?.orgAgentTypeId, agentEndPoint, agentDetails?.tenantId); + + const apiKey = user?.apiKey; + + const createConnectionInvitation = await this._createConnectionInvitation(connectionPayload, url, apiKey); + + const connectionInvitationUrl = createConnectionInvitation.message.invitationUrl; + const referenceId: string = uuid(); + await this.storeShorteningUrl(referenceId, connectionInvitationUrl); + const shortenedUrl = `${process.env.API_GATEWAY_PROTOCOL}://${process.env.API_ENDPOINT}/connections/url/${referenceId}`; + const saveConnectionDetails = await this.connectionRepository.saveAgentConnectionInvitations(shortenedUrl, agentId, orgId); + return saveConnectionDetails; + } catch (error) { + this.logger.error(`[createLegacyConnectionInvitation] - error in connection invitation: ${error}`); + throw new RpcException(error.response); + } + } + + + /** + * Description: create connection legacy invitation + * @param orgId + * @param user + * @returns Connection legacy invitation URL + */ + async getConnectionWebhook( + createDateTime: string, lastChangedDateTime: string, connectionId: string, state: string, orgDid: string, theirLabel: string, autoAcceptConnection: boolean, outOfBandId: string, orgId: number + ): Promise { + try { + const saveConnectionDetails = await this.connectionRepository.saveConnectionWebhook(createDateTime, lastChangedDateTime, connectionId, state, orgDid, theirLabel, autoAcceptConnection, outOfBandId, orgId); + return saveConnectionDetails; + } catch (error) { + this.logger.error(`[getConnectionWebhook] - error in fetch connection webhook: ${error}`); + throw new RpcException(error.response); + } + } + + + /** + * Description: Store shortening URL + * @param referenceId + * @param url + * @returns connection invitation URL + */ + async _createConnectionInvitation(connectionPayload: object, url: string, apiKey: string): Promise<{ + message: { + invitationUrl: string; + }; + }> { + const pattern = { cmd: 'agent-create-connection-legacy-invitation' }; + const payload = { connectionPayload, url, apiKey }; + return this.connectionServiceProxy + .send<{ invitationUrl: string }>(pattern, payload) + .pipe( + map((message) => ({ message })) + ).toPromise() + .catch(error => { + this.logger.error(`catch: ${JSON.stringify(error)}`); + throw new HttpException({ + status: error.status, + error: error.message + }, error.status); + }); + } + + async storeShorteningUrl(referenceId: string, connectionInvitationUrl: string): Promise { + try { + return this.connectionRepository.storeShorteningUrl(referenceId, connectionInvitationUrl); + + } catch (error) { + this.logger.error(`Error in store agent details : ${JSON.stringify(error)}`); + throw error; + } + } + + /** + * Description: Fetch connection invitaion by referenceId + * @param referenceId + * @returns Connection legacy invitation URL + */ + async getUrl( + referenceId: string): Promise { + try { + const urlDetails = await this.connectionRepository.getShorteningUrl(referenceId); + return urlDetails.url; + } catch (error) { + this.logger.error(`Error in get url in connection service: ${JSON.stringify(error)}`); + throw error; + + } + } + + /** + * Description: Fetch all connections + * @param outOfBandId + * @param alias + * @param state + * @param myDid + * @param theirDid + * @param theirLabel + * @param orgId + * @param user + * + * @returns get all connections details + */ + async getConnections(user: IUserRequest, outOfBandId: string, alias: string, state: string, myDid: string, theirDid: string, theirLabel: string, orgId: number): Promise { + try { + const agentDetails = await this.connectionRepository.getAgentEndPoint(orgId); + const { agentEndPoint } = agentDetails; + if (!agentDetails) { + throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); + } + const params = { + outOfBandId, + alias, + state, + myDid, + theirDid, + theirLabel + }; + let url = `${agentEndPoint}${CommonConstants.URL_CONN_GET_CONNECTIONS}`; + Object.keys(params).forEach((element: string) => { + const appendParams: string = url.includes('?') ? '&' : '?'; + + if (params[element] !== undefined) { + url = `${url + appendParams + element}=${params[element]}`; + } + }); + const apiKey = user?.apiKey; + const connectionsDetails = await this._getAllConnections(url, apiKey); + return connectionsDetails?.response; + } catch (error) { + this.logger.error(`Error in get url in connection service: ${JSON.stringify(error)}`); + throw error; + + } + } + + + async _getAllConnections(url: string, apiKey: 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); + }); + } catch (error) { + this.logger.error(`[_getAllConnections] [NATS call]- error in fetch connections details : ${JSON.stringify(error)}`); + throw error; + } + } + + async getConnectionsById(user: IUserRequest, connectionId: string, orgId: number): Promise { + try { + + const agentDetails = await this.connectionRepository.getAgentEndPoint(orgId); + const { agentEndPoint } = agentDetails; + if (!agentDetails) { + throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); + } + const url = `${agentEndPoint}${CommonConstants.URL_CONN_GET_CONNECTIONS}/${connectionId}`; + const apiKey = user?.apiKey; + const createConnectionInvitation = await this._getConnectionsByConnectionId(url, apiKey); + return createConnectionInvitation?.response; + } catch (error) { + this.logger.error(`[getConnectionsById] - error in get connections : ${JSON.stringify(error)}`); + throw error; + } + } + + async _getConnectionsByConnectionId(url: string, apiKey: string): Promise<{ + response: string; + }> { + try { + const pattern = { cmd: 'agent-get-connections-by-connectionId' }; + 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); + }); + } catch (error) { + this.logger.error(`[_getConnectionsByConnectionId] [NATS call]- error in fetch connections : ${JSON.stringify(error)}`); + throw error; + } + } + /** + * Description: Fetch agent url + * @param referenceId + * @returns agent URL + */ + async getAgentUrl( + orgAgentTypeId: number, + agentEndPoint: string, + tenantId?: string + ): Promise { + try { + + let url; + if (orgAgentTypeId === OrgAgentType.DEDICATED) { + + url = `${agentEndPoint}${CommonConstants.URL_CONN_LEGACY_INVITE}`; + } else if (orgAgentTypeId === OrgAgentType.SHARED) { + + url = `${agentEndPoint}${CommonConstants.URL_SHAGENT_CREATE_INVITATION}`.replace('#', tenantId); + } else { + + throw new NotFoundException(ResponseMessages.connection.error.agentUrlNotFound); + } + return url; + + } catch (error) { + this.logger.error(`Error in get agent url: ${JSON.stringify(error)}`); + throw error; + + } + } +} diff --git a/apps/connection/src/enum.ts b/apps/connection/src/enum.ts new file mode 100644 index 000000000..0376d7b99 --- /dev/null +++ b/apps/connection/src/enum.ts @@ -0,0 +1,4 @@ +export enum CredentialSortBy { + id = 'id', + createDateTime = 'createDateTime' +} \ No newline at end of file diff --git a/apps/connection/src/interfaces/connection.interfaces.ts b/apps/connection/src/interfaces/connection.interfaces.ts new file mode 100644 index 000000000..885a8cf0d --- /dev/null +++ b/apps/connection/src/interfaces/connection.interfaces.ts @@ -0,0 +1,82 @@ +// eslint-disable-next-line camelcase +import { IUserRequest } from '@credebl/user-request/user-request.interface'; +import { UserRoleOrgPermsDto } from 'apps/api-gateway/src/dtos/user-role-org-perms.dto'; + +export interface IConnection { + user: IUserRequestInterface, + alias: string; + label: string; + imageUrl: string; + multiUseInvitation: boolean; + autoAcceptConnection: boolean; + orgId: number; +} +export interface IUserRequestInterface { + userId: number; + email: string; + orgId: number; + agentEndPoint?: string; + apiKey?: string; + tenantId?: number; + tenantName?: string; + tenantOrgId?: number; + userRoleOrgPermissions?: UserRoleOrgPermsDto[]; + orgName?: string; + selectedOrg: ISelectedOrgInterface; +} + +export interface ISelectedOrgInterface { + id: number; + userId: number; + orgRoleId: number; + orgId: number; + orgRole: object; + organisation: object; +} + +export interface IOrganizationInterface { + name: string; + description: string; + org_agents: IOrgAgentInterface[] + +} + +export interface IOrgAgentInterface { + orgDid: string; + verkey: string; + agentEndPoint: string; + agentOptions: string; + walletName: string; + agentsTypeId: string; + orgId: string; +} + + +export class IConnectionInterface { + createDateTime: string; + lastChangedDateTime: string; + connectionId: string; + state: string; + orgDid?: string; + theirLabel: string; + autoAcceptConnection: boolean; + outOfBandId: string; + orgId: number; +} + +export class IFetchConnectionInterface { + user: IUserRequest; + outOfBandId: string; + alias: string; + state: string; + myDid: string; + theirDid: string; + theirLabel: string; + orgId: number; +} + +export interface IFetchConnectionById { + user: IUserRequest; + connectionId: string; + orgId: number; +} \ No newline at end of file diff --git a/apps/connection/src/main.ts b/apps/connection/src/main.ts new file mode 100644 index 000000000..345e7a0f9 --- /dev/null +++ b/apps/connection/src/main.ts @@ -0,0 +1,23 @@ +import { NestFactory } from '@nestjs/core'; +import { ConnectionModule } from './connection.module'; +import { HttpExceptionFilter } from 'libs/http-exception.filter'; +import { Logger } from '@nestjs/common'; +import { MicroserviceOptions, Transport } from '@nestjs/microservices'; + +const logger = new Logger(); + +async function bootstrap(): Promise { + + const app = await NestFactory.createMicroservice(ConnectionModule, { + transport: Transport.NATS, + options: { + servers: [`${process.env.NATS_URL}`] + } + }); + + app.useGlobalFilters(new HttpExceptionFilter()); + + await app.listen(); + logger.log('Connection-Service Microservice is listening to NATS '); +} +bootstrap(); diff --git a/apps/connection/test/app.e2e-spec.ts b/apps/connection/test/app.e2e-spec.ts new file mode 100644 index 000000000..bae2e58fd --- /dev/null +++ b/apps/connection/test/app.e2e-spec.ts @@ -0,0 +1,22 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { INestApplication } from '@nestjs/common'; +import * as request from 'supertest'; +import { ConnectionModule } from '../src/connection.module'; + +describe('ConnectionServiceController (e2e)', () => { + let app: INestApplication; + + beforeEach(async () => { + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [ConnectionModule] + }).compile(); + + app = moduleFixture.createNestApplication(); + await app.init(); + }); + + it('/ (GET)', () => request(app.getHttpServer()) + .get('/') + .expect(200) + .expect('Hello World!')); +}); diff --git a/apps/connection/test/jest-e2e.json b/apps/connection/test/jest-e2e.json new file mode 100644 index 000000000..e9d912f3e --- /dev/null +++ b/apps/connection/test/jest-e2e.json @@ -0,0 +1,9 @@ +{ + "moduleFileExtensions": ["js", "json", "ts"], + "rootDir": ".", + "testEnvironment": "node", + "testRegex": ".e2e-spec.ts$", + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + } +} diff --git a/apps/connection/tsconfig.app.json b/apps/connection/tsconfig.app.json new file mode 100644 index 000000000..312d6801f --- /dev/null +++ b/apps/connection/tsconfig.app.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "declaration": false, + "outDir": "../../dist/apps/connection" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] +} diff --git a/apps/issuance/Dockerfile b/apps/issuance/Dockerfile new file mode 100644 index 000000000..5373e58cf --- /dev/null +++ b/apps/issuance/Dockerfile @@ -0,0 +1,43 @@ +# Stage 1: Build the application +FROM node:16-alpine as build + +# Set the working directory +WORKDIR /app + +# Copy package.json and package-lock.json +COPY package.json ./ +#COPY package-lock.json ./ + +# Install dependencies +RUN npm i + +# Copy the rest of the application code +COPY . . +CMD ["sh", "-c", "cd libs/prisma-service && npx prisma generate"] + +# Build the user service +RUN npm run build issuance + +# Stage 2: Create the final image +FROM node:16-alpine + +# Set the working directory +WORKDIR /app + +# Copy the compiled code from the build stage +COPY --from=build /app/dist/apps/issuance/ ./dist/apps/issuance/ + +# 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 + +# Install production dependencies for the final image +#RUN npm i --only=production + +# Set the command to run the microservice +CMD ["sh", "-c", "cd libs/prisma-service && npx prisma generate && cd ../.. && node dist/apps/issuance/main.js"] + +# docker build -t issuance -f apps/issuance/Dockerfile . +# docker run -d --env-file .env --name issuance docker.io/library/issuance +# docker logs -f issuance diff --git a/apps/issuance/interfaces/issuance.interfaces.ts b/apps/issuance/interfaces/issuance.interfaces.ts new file mode 100644 index 000000000..756c10e51 --- /dev/null +++ b/apps/issuance/interfaces/issuance.interfaces.ts @@ -0,0 +1,45 @@ +// eslint-disable-next-line camelcase +import { IUserRequest } from '@credebl/user-request/user-request.interface'; + + +export interface IAttributes { + name: string; + value: string; +} +export interface IIssuance { + user: IUserRequest; + credentialDefinitionId: string; + comment: string; + connectionId: string; + attributes: IAttributes[]; + orgId: number; +} + +export interface IIssueCredentials { + user: IUserRequest; + connectionId: string; + threadId: string; + orgId: number; + state: string; +} + +export interface IIssueCredentialsDefinitions { + user: IUserRequest; + credentialRecordId: string; + orgId: number; +} + +export interface IIssuanceWebhookInterface { + createDateTime: string; + connectionId: string; + threadId: string; + protocolVersion: string; + credentialAttributes: ICredentialAttributesInterface[]; + orgId: number; +} + +export interface ICredentialAttributesInterface { + 'mime-type': string; + name: string; + value: string; +} diff --git a/apps/issuance/src/issuance.controller.ts b/apps/issuance/src/issuance.controller.ts new file mode 100644 index 000000000..6035bcd50 --- /dev/null +++ b/apps/issuance/src/issuance.controller.ts @@ -0,0 +1,40 @@ +import { Controller, Logger } from '@nestjs/common'; +import { MessagePattern } from '@nestjs/microservices'; +import { IIssuance, IIssuanceWebhookInterface, IIssueCredentials, IIssueCredentialsDefinitions } from '../interfaces/issuance.interfaces'; +import { IssuanceService } from './issuance.service'; + +@Controller() +export class IssuanceController { + private readonly logger = new Logger('issuanceService'); + constructor(private readonly issuanceService: IssuanceService) { } + + @MessagePattern({ cmd: 'send-credential-create-offer' }) + async sendCredentialCreateOffer(payload: IIssuance): Promise { + const { orgId, user, credentialDefinitionId, comment, connectionId, attributes } = payload; + return this.issuanceService.sendCredentialCreateOffer(orgId, user, credentialDefinitionId, comment, connectionId, attributes); + } + + @MessagePattern({ cmd: 'send-credential-create-offer-oob' }) + async sendCredentialOutOfBand(payload: IIssuance): Promise { + const { orgId, user, credentialDefinitionId, comment, connectionId, attributes } = payload; + return this.issuanceService.sendCredentialOutOfBand(orgId, user, credentialDefinitionId, comment, connectionId, attributes); + } + + @MessagePattern({ cmd: 'get-all-issued-credentials' }) + async getIssueCredentials(payload: IIssueCredentials): Promise { + const { user, threadId, connectionId, state, orgId } = payload; + return this.issuanceService.getIssueCredentials(user, threadId, connectionId, state, orgId); + } + + @MessagePattern({ cmd: 'get-issued-credentials-by-credentialDefinitionId' }) + async getIssueCredentialsbyCredentialRecordId(payload: IIssueCredentialsDefinitions): Promise { + const { user, credentialRecordId, orgId } = payload; + return this.issuanceService.getIssueCredentialsbyCredentialRecordId(user, credentialRecordId, orgId); + } + + @MessagePattern({ cmd: 'webhook-get-issue-credential' }) + async getIssueCredentialWebhook(payload: IIssuanceWebhookInterface): Promise { + const { createDateTime, connectionId, threadId, protocolVersion, credentialAttributes, orgId } = payload; + return this.issuanceService.getIssueCredentialWebhook(createDateTime, connectionId, threadId, protocolVersion, credentialAttributes, orgId); + } +} diff --git a/apps/issuance/src/issuance.module.ts b/apps/issuance/src/issuance.module.ts new file mode 100644 index 000000000..732e40f35 --- /dev/null +++ b/apps/issuance/src/issuance.module.ts @@ -0,0 +1,27 @@ +import { CommonModule } from '@credebl/common'; +import { PrismaService } from '@credebl/prisma-service'; +import { Logger, Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { ClientsModule, Transport } from '@nestjs/microservices'; +import { IssuanceController } from './issuance.controller'; +import { IssuanceRepository } from './issuance.repository'; +import { IssuanceService } from './issuance.service'; + +@Module({ + imports: [ + ConfigModule.forRoot(), + ClientsModule.register([ + { + name: 'NATS_CLIENT', + transport: Transport.NATS, + options: { + servers: [`${process.env.NATS_URL}`] + } + } + ]), + CommonModule + ], + controllers: [IssuanceController], + providers: [IssuanceService, IssuanceRepository, PrismaService, Logger] +}) +export class IssuanceModule { } diff --git a/apps/issuance/src/issuance.repository.ts b/apps/issuance/src/issuance.repository.ts new file mode 100644 index 000000000..6281f6e55 --- /dev/null +++ b/apps/issuance/src/issuance.repository.ts @@ -0,0 +1,126 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { PrismaService } from '@credebl/prisma-service'; +// eslint-disable-next-line camelcase +import { agent_invitations, credentials, org_agents, shortening_url } from '@prisma/client'; +@Injectable() +export class IssuanceRepository { + + constructor( + private readonly prisma: PrismaService, + private readonly logger: Logger + ) { } + + /** + * Description: Get getAgentEndPoint by orgId + * @param connectionId + * @returns Get getAgentEndPoint details + */ + // eslint-disable-next-line camelcase + async getAgentEndPoint(orgId: number): Promise { + try { + + const agentDetails = await this.prisma.org_agents.findFirst({ + where: { + orgId + } + }); + return agentDetails; + + } catch (error) { + this.logger.error(`Error in get getAgentEndPoint: ${error.message} `); + throw error; + } + } + + + /** + * Description: save credentials + * @param connectionId + * @returns Get saved credential details + */ + // eslint-disable-next-line camelcase + async saveIssuedCredentialDetails(createDateTime: string, connectionId: string, threadId: string, protocolVersion: string, credentialAttributes: object[], orgId: number): Promise { + try { + + const credentialDetails = await this.prisma.credentials.upsert({ + where: { + connectionId + }, + update: { + lastChangedBy: orgId, + createDateTime, + threadId, + protocolVersion, + credentialAttributes, + orgId + }, + create: { + createDateTime, + lastChangedBy: orgId, + connectionId, + threadId, + protocolVersion, + credentialAttributes, + orgId + } + }); + return credentialDetails; + + } catch (error) { + this.logger.error(`Error in get saveIssuedCredentialDetails: ${error.message} `); + throw error; + } + } + + /** + * Description: Save connection details + * @param connectionInvitation + * @param agentId + * @param orgId + * @returns Get connection details + */ + // eslint-disable-next-line camelcase + async saveAgentConnectionInvitations(connectionInvitation: string, agentId: number, orgId: number): Promise { + try { + + const agentDetails = await this.prisma.agent_invitations.create({ + data: { + orgId, + agentId, + connectionInvitation, + multiUse: true + } + }); + return agentDetails; + + } catch (error) { + this.logger.error(`Error in saveAgentConnectionInvitations: ${error.message} `); + throw error; + } + } + + /** + * Description: Save ShorteningUrl details + * @param referenceId + * @param connectionInvitationUrl + * @returns Get storeShorteningUrl details + */ + // eslint-disable-next-line camelcase + async storeShorteningUrl(referenceId: string, connectionInvitationUrl: string): Promise { + try { + + return this.prisma.shortening_url.create({ + data: { + referenceId, + url: connectionInvitationUrl, + type: null + } + }); + + } catch (error) { + this.logger.error(`Error in saveAgentConnectionInvitations: ${error.message} `); + throw error; + } + } + +} \ No newline at end of file diff --git a/apps/issuance/src/issuance.service.ts b/apps/issuance/src/issuance.service.ts new file mode 100644 index 000000000..d4128cd7c --- /dev/null +++ b/apps/issuance/src/issuance.service.ts @@ -0,0 +1,305 @@ +import { CommonService } from '@credebl/common'; +import { HttpException, Inject, Injectable, 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'; +import { ResponseMessages } from '@credebl/common/response-messages'; +import { ClientProxy, RpcException } from '@nestjs/microservices'; +import { map } from 'rxjs'; +import { ICredentialAttributesInterface } from '../interfaces/issuance.interfaces'; +import { OrgAgentType } from '@credebl/enum/enum'; + + +@Injectable() +export class IssuanceService { + private readonly logger = new Logger('IssueCredentialService'); + constructor( + @Inject('NATS_CLIENT') private readonly issuanceServiceProxy: ClientProxy, + private readonly commonService: CommonService, + private readonly issuanceRepository: IssuanceRepository + + ) { } + + + async sendCredentialCreateOffer(orgId: number, user: IUserRequest, credentialDefinitionId: string, comment: string, connectionId: string, attributes: object[]): Promise { + try { + const agentDetails = await this.issuanceRepository.getAgentEndPoint(orgId); + const { agentEndPoint } = agentDetails; + if (!agentDetails) { + throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); + } + + const issuanceMethodLabel = 'create-offer'; + const url = await this.getAgentUrl(issuanceMethodLabel, agentDetails?.orgAgentTypeId, agentEndPoint, agentDetails?.tenantId); + + const apiKey = user?.apiKey; + const issueData = { + connectionId, + credentialFormats: { + indy: { + attributes, + credentialDefinitionId + } + }, + autoAcceptCredential: 'always', + comment + }; + + const credentialCreateOfferDetails = await this._sendCredentialCreateOffer(issueData, url, apiKey); + + return credentialCreateOfferDetails?.response; + } catch (error) { + this.logger.error(`[sendCredentialCreateOffer] - error in create credentials : ${JSON.stringify(error)}`); + throw error; + } + } + + + async sendCredentialOutOfBand(orgId: number, user: IUserRequest, credentialDefinitionId: string, comment: string, connectionId: string, attributes: object[]): Promise { + try { + const agentDetails = await this.issuanceRepository.getAgentEndPoint(orgId); + const { agentEndPoint } = agentDetails; + if (!agentDetails) { + throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); + } + + const issuanceMethodLabel = 'create-offer-oob'; + const url = await this.getAgentUrl(issuanceMethodLabel, agentDetails?.orgAgentTypeId, agentEndPoint, agentDetails?.tenantId); + + const apiKey = user?.apiKey; + const issueData = { + connectionId, + credentialFormats: { + indy: { + attributes, + credentialDefinitionId + } + }, + autoAcceptCredential: 'always', + comment + }; + const credentialCreateOfferDetails = await this._sendCredentialCreateOffer(issueData, url, apiKey); + return credentialCreateOfferDetails?.response; + } catch (error) { + this.logger.error(`[sendCredentialCreateOffer] - error in create credentials : ${JSON.stringify(error)}`); + throw error; + } + } + + async _sendCredentialCreateOffer(issueData: object, url: string, apiKey: string): Promise<{ + response: string; + }> { + try { + const pattern = { cmd: 'agent-send-credential-create-offer' }; + const payload = { issueData, url, apiKey }; + return this.issuanceServiceProxy + .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(`[_sendCredentialCreateOffer] [NATS call]- error in create credentials : ${JSON.stringify(error)}`); + throw error; + } + } + + async getIssueCredentials(user: IUserRequest, threadId: string, connectionId: string, state: string, orgId: number): Promise { + try { + const agentDetails = await this.issuanceRepository.getAgentEndPoint(orgId); + const { agentEndPoint } = agentDetails; + if (!agentDetails) { + throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); + } + const params = { + threadId, + connectionId, + state + }; + + const issuanceMethodLabel = 'get-issue-credentials'; + let url = await this.getAgentUrl(issuanceMethodLabel, agentDetails?.orgAgentTypeId, agentEndPoint, agentDetails?.tenantId); + + Object.keys(params).forEach((element: string) => { + const appendParams: string = url.includes('?') ? '&' : '?'; + + if (params[element] !== undefined) { + url = `${url + appendParams + element}=${params[element]}`; + } + }); + const apiKey = user?.apiKey; + const issueCredentialsDetails = await this._getIssueCredentials(url, apiKey); + return issueCredentialsDetails?.response; + } catch (error) { + this.logger.error(`[sendCredentialCreateOffer] - error in create credentials : ${JSON.stringify(error)}`); + throw new RpcException(error); + } + } + + async _getIssueCredentials(url: string, apiKey: string): Promise<{ + response: string; + }> { + try { + const pattern = { cmd: 'agent-get-all-issued-credentials' }; + const payload = { url, apiKey }; + return this.issuanceServiceProxy + .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(`[_getIssueCredentials] [NATS call]- error in fetch credentials : ${JSON.stringify(error)}`); + throw error; + } + } + + async getIssueCredentialsbyCredentialRecordId(user: IUserRequest, credentialRecordId: string, orgId: number): Promise { + try { + + const agentDetails = await this.issuanceRepository.getAgentEndPoint(orgId); + const { agentEndPoint } = agentDetails; + if (!agentDetails) { + throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); + } + + const issuanceMethodLabel = 'get-issue-credential-by-credential-id'; + const url = await this.getAgentUrl(issuanceMethodLabel, agentDetails?.orgAgentTypeId, agentEndPoint, agentDetails?.tenantId, credentialRecordId); + + const apiKey = user?.apiKey; + const createConnectionInvitation = await this._getIssueCredentialsbyCredentialRecordId(url, apiKey); + return createConnectionInvitation?.response; + } catch (error) { + this.logger.error(`[getIssueCredentialsbyCredentialRecordId] - error in get credentials : ${JSON.stringify(error)}`); + throw error; + } + } + + async getIssueCredentialWebhook(createDateTime: string, connectionId: string, threadId: string, protocolVersion: string, credentialAttributes: ICredentialAttributesInterface[], orgId: number): Promise { + try { + const agentDetails = await this.issuanceRepository.saveIssuedCredentialDetails(createDateTime, connectionId, threadId, protocolVersion, credentialAttributes, orgId); + return agentDetails; + } catch (error) { + this.logger.error(`[getIssueCredentialsbyCredentialRecordId] - error in get credentials : ${JSON.stringify(error)}`); + throw error; + } + } + + async _getIssueCredentialsbyCredentialRecordId(url: string, apiKey: string): Promise<{ + response: string; + }> { + try { + const pattern = { cmd: 'agent-get-issued-credentials-by-credentialDefinitionId' }; + const payload = { url, apiKey }; + return this.issuanceServiceProxy + .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(`[_getIssueCredentialsbyCredentialRecordId] [NATS call]- error in fetch credentials : ${JSON.stringify(error)}`); + throw error; + } + } + + /** + * Description: Fetch agent url + * @param referenceId + * @returns agent URL + */ + async getAgentUrl( + issuanceMethodLabel: string, + orgAgentTypeId: number, + agentEndPoint: string, + tenantId: string, + credentialRecordId?: string + ): Promise { + try { + + let url; + switch (issuanceMethodLabel) { + case 'create-offer': { + url = orgAgentTypeId === OrgAgentType.DEDICATED + ? `${agentEndPoint}${CommonConstants.URL_ISSUE_CREATE_CRED_OFFER_AFJ}` + : orgAgentTypeId === OrgAgentType.SHARED + ? `${agentEndPoint}${CommonConstants.URL_SHAGENT_CREATE_OFFER}`.replace('#', tenantId) + : null; + break; + } + + case 'create-offer-oob': { + url = orgAgentTypeId === OrgAgentType.DEDICATED + ? `${agentEndPoint}${CommonConstants.URL_ISSUE_CREATE_CRED_OFFER_AFJ}` + : orgAgentTypeId === OrgAgentType.SHARED + ? `${agentEndPoint}${CommonConstants.URL_SHAGENT_CREATE_OFFER_OUT_OF_BAND}`.replace('#', tenantId) + : null; + break; + } + + case 'get-issue-credentials': { + url = orgAgentTypeId === OrgAgentType.DEDICATED + ? `${agentEndPoint}${CommonConstants.URL_ISSUE_GET_CREDS_AFJ}` + : orgAgentTypeId === OrgAgentType.SHARED + ? `${agentEndPoint}${CommonConstants.URL_SHAGENT_GET_CREDENTIALS}`.replace('#', tenantId) + : null; + break; + } + + case 'get-issue-credential-by-credential-id': { + + url = orgAgentTypeId === OrgAgentType.DEDICATED + ? `${agentEndPoint}${CommonConstants.URL_ISSUE_GET_CREDS_AFJ_BY_CRED_REC_ID}/${credentialRecordId}` + : orgAgentTypeId === OrgAgentType.SHARED + ? `${agentEndPoint}${CommonConstants.URL_SHAGENT_GET_CREDENTIALS_BY_CREDENTIAL_ID}`.replace('#', credentialRecordId).replace('@', tenantId) + : null; + break; + } + + default: { + break; + } + } + + if (!url) { + throw new NotFoundException(ResponseMessages.issuance.error.agentUrlNotFound); + } + + return url; + } catch (error) { + this.logger.error(`Error in get agent url: ${JSON.stringify(error)}`); + throw error; + + } + } +} diff --git a/apps/issuance/src/main.ts b/apps/issuance/src/main.ts new file mode 100644 index 000000000..b08fcd872 --- /dev/null +++ b/apps/issuance/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 { IssuanceModule } from '../src/issuance.module'; + +const logger = new Logger(); + +async function bootstrap(): Promise { + + const app = await NestFactory.createMicroservice(IssuanceModule, { + transport: Transport.NATS, + options: { + servers: [`${process.env.NATS_URL}`] + } + }); + + app.useGlobalFilters(new HttpExceptionFilter()); + + await app.listen(); + logger.log('Issuance-Service Microservice is listening to NATS '); +} +bootstrap(); diff --git a/apps/issuance/tsconfig.app.json b/apps/issuance/tsconfig.app.json new file mode 100644 index 000000000..5ffc426af --- /dev/null +++ b/apps/issuance/tsconfig.app.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "declaration": false, + "outDir": "../../dist/apps/issuance" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] +} diff --git a/apps/ledger/Dockerfile b/apps/ledger/Dockerfile new file mode 100644 index 000000000..edb39ec66 --- /dev/null +++ b/apps/ledger/Dockerfile @@ -0,0 +1,43 @@ +# Stage 1: Build the application +FROM node:16-alpine as build + +# Set the working directory +WORKDIR /app + +# Copy package.json and package-lock.json +COPY package.json ./ +#COPY package-lock.json ./ + +# Install dependencies +RUN npm i + +# Copy the rest of the application code +COPY . . +CMD ["sh", "-c", "cd libs/prisma-service && npx prisma generate"] + +# Build the user service +RUN npm run build ledger + +# Stage 2: Create the final image +FROM node:16-alpine + +# Set the working directory +WORKDIR /app + +# Copy the compiled code from the build stage +COPY --from=build /app/dist/apps/ledger/ ./dist/apps/ledger/ + +# 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 + +# Install production dependencies for the final image +#RUN npm i --only=production + +# Set the command to run the microservice +CMD ["sh", "-c", "cd libs/prisma-service && npx prisma generate && cd ../.. && node dist/apps/ledger/main.js"] + +# docker build -t ledger -f apps/ledger/Dockerfile . +# docker run -d --env-file .env --name ledger docker.io/library/ledger +# docker logs -f ledger diff --git a/apps/ledger/src/credential-definition/credential-definition.controller.ts b/apps/ledger/src/credential-definition/credential-definition.controller.ts new file mode 100644 index 000000000..7ef0d638c --- /dev/null +++ b/apps/ledger/src/credential-definition/credential-definition.controller.ts @@ -0,0 +1,48 @@ +/* eslint-disable camelcase */ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { Controller, Logger } from '@nestjs/common'; + +import { CredentialDefinitionService } from './credential-definition.service'; +import { MessagePattern } from '@nestjs/microservices'; +import { GetAllCredDefsPayload } from './interfaces/create-credential-definition.interface'; +import { CreateCredDefPayload, GetCredDefPayload } from './interfaces/create-credential-definition.interface'; +import { credential_definition } from '@prisma/client'; + +@Controller('credential-definitions') +export class CredentialDefinitionController { + private logger = new Logger(); + + constructor(private readonly credDefService: CredentialDefinitionService) { } + + @MessagePattern({ cmd: 'create-credential-definition' }) + async createCredentialDefinition(payload: CreateCredDefPayload): Promise { + return this.credDefService.createCredentialDefinition(payload); + } + + @MessagePattern({ cmd: 'get-credential-definition-by-id' }) + async getCredentialDefinitionById(payload: GetCredDefPayload): Promise { + return this.credDefService.getCredentialDefinitionById(payload); + } + + @MessagePattern({ cmd: 'get-all-credential-definitions' }) + async getAllCredDefs(payload: GetAllCredDefsPayload): Promise<{ + totalItems: number; + hasNextPage: boolean; + hasPreviousPage: boolean; + nextPage: number; + previousPage: number; + lastPage: number; + data: { + createDateTime: Date; + createdBy: number; + credentialDefinitionId: string; + tag: string; + schemaLedgerId: string; + schemaId: number; + orgId: number; + revocable: boolean; + }[] + }> { + return this.credDefService.getAllCredDefs(payload); + } +} \ No newline at end of file diff --git a/apps/ledger/src/credential-definition/credential-definition.module.ts b/apps/ledger/src/credential-definition/credential-definition.module.ts new file mode 100644 index 000000000..11d80f2e4 --- /dev/null +++ b/apps/ledger/src/credential-definition/credential-definition.module.ts @@ -0,0 +1,33 @@ +import { ClientsModule, Transport } from '@nestjs/microservices'; +import { Logger, Module } from '@nestjs/common'; + +import { CommonModule } from '@credebl/common'; +import { CredentialDefinitionController } from './credential-definition.controller'; +import { CredentialDefinitionRepository } from './repositories/credential-definition.repository'; +import { CredentialDefinitionService } from './credential-definition.service'; +import { HttpModule } from '@nestjs/axios'; +import { PrismaService } from '@credebl/prisma-service'; + +@Module({ + imports: [ + ClientsModule.register([ + { + name: 'NATS_CLIENT', + transport: Transport.NATS, + options: { + servers: [`${process.env.NATS_URL}`] + } + } + ]), + HttpModule, + CommonModule + ], + providers: [ + CredentialDefinitionService, + CredentialDefinitionRepository, + Logger, + PrismaService + ], + controllers: [CredentialDefinitionController] +}) +export class CredentialDefinitionModule { } diff --git a/apps/ledger/src/credential-definition/credential-definition.service.ts b/apps/ledger/src/credential-definition/credential-definition.service.ts new file mode 100644 index 000000000..eb8831fcf --- /dev/null +++ b/apps/ledger/src/credential-definition/credential-definition.service.ts @@ -0,0 +1,259 @@ +/* eslint-disable camelcase */ +import { + ConflictException, + HttpException, + Inject, + Injectable, + NotFoundException +} from '@nestjs/common'; +import { ClientProxy, RpcException } from '@nestjs/microservices'; +import { BaseService } from 'libs/service/base.service'; +import { CredentialDefinitionRepository } from './repositories/credential-definition.repository'; +import { CreateCredDefPayload, CredDefPayload, GetAllCredDefsPayload, GetCredDefPayload } from './interfaces/create-credential-definition.interface'; +import { credential_definition } from '@prisma/client'; +import { ResponseMessages } from '@credebl/common/response-messages'; +import { CreateCredDefAgentRedirection, GetCredDefAgentRedirection } from './interfaces/credential-definition.interface'; +import { map } from 'rxjs/operators'; + +@Injectable() +export class CredentialDefinitionService extends BaseService { + constructor( + private readonly credentialDefinitionRepository: CredentialDefinitionRepository, + @Inject('NATS_CLIENT') private readonly credDefServiceProxy: ClientProxy + + ) { + super('CredentialDefinitionService'); + } + + async createCredentialDefinition(payload: CreateCredDefPayload): Promise { + try { + const { credDef, user } = payload; + const { agentEndPoint, orgDid } = await this.credentialDefinitionRepository.getAgentDetailsByOrgId(credDef.orgId); + // 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 = ''; + const { userId } = user.selectedOrg; + credDef.tag = credDef.tag.trim(); + const dbResult: credential_definition = await this.credentialDefinitionRepository.getByAttribute( + credDef.schemaLedgerId, + credDef.tag + ); + + if (dbResult) { + throw new ConflictException(ResponseMessages.credentialDefinition.error.Conflict); + } + let credDefResponseFromAgentService; + if (1 === getAgentDetails.org_agents[0].orgAgentTypeId) { + const CredDefPayload = { + tag: credDef.tag, + schemaId: credDef.schemaLedgerId, + issuerId: did, + agentEndPoint, + apiKey, + agentType: 1 + }; + credDefResponseFromAgentService = await this._createCredentialDefinition(CredDefPayload); + } else if (2 === getAgentDetails.org_agents[0].orgAgentTypeId) { + const { tenantId } = await this.credentialDefinitionRepository.getAgentDetailsByOrgId(credDef.orgId); + + const CredDefPayload = { + tenantId, + method: 'registerCredentialDefinition', + payload: { + tag: credDef.tag, + schemaId: credDef.schemaLedgerId, + issuerId: did + }, + agentEndPoint, + apiKey, + agentType: 2 + }; + credDefResponseFromAgentService = await this._createCredentialDefinition(CredDefPayload); + } + const response = JSON.parse(JSON.stringify(credDefResponseFromAgentService.response)); + const schemaDetails = await this.credentialDefinitionRepository.getSchemaById(credDef.schemaLedgerId); + if (!schemaDetails) { + throw new NotFoundException(ResponseMessages.credentialDefinition.error.schemaIdNotFound); + } + const credDefData: CredDefPayload = { + tag: '', + schemaLedgerId: '', + issuerId: '', + revocable: credDef.revocable, + createdBy: 0, + orgId: 0, + schemaId: 0, + credentialDefinitionId: '' + }; + + if ('finished' === response.state) { + credDefData.tag = response.credentialDefinition.tag; + credDefData.schemaLedgerId = response.credentialDefinition.schemaId; + credDefData.issuerId = response.credentialDefinition.issuerId; + credDefData.credentialDefinitionId = response.credentialDefinitionId; + credDefData.orgId = credDef.orgId; + credDefData.revocable = credDef.revocable; + credDefData.schemaId = schemaDetails.id; + credDefData.createdBy = userId; + } else if ('finished' === response.credentialDefinition.state) { + credDefData.tag = response.credentialDefinition.credentialDefinition.tag; + credDefData.schemaLedgerId = response.credentialDefinition.credentialDefinition.schemaId; + credDefData.issuerId = response.credentialDefinition.credentialDefinition.issuerId; + credDefData.credentialDefinitionId = response.credentialDefinition.credentialDefinitionId; + credDefData.orgId = credDef.orgId; + credDefData.revocable = credDef.revocable; + credDefData.schemaId = schemaDetails.id; + credDefData.createdBy = userId; + } + const credDefResponse = await this.credentialDefinitionRepository.saveCredentialDefinition(credDefData); + return credDefResponse; + + } catch (error) { + this.logger.error( + `Error in creating credential definition: ${JSON.stringify(error)}` + ); + throw new RpcException(error.response); + } + } + + async _createCredentialDefinition(payload: CreateCredDefAgentRedirection): Promise<{ + response: string; + }> { + try { + const pattern = { + cmd: 'agent-create-credential-definition' + }; + const credDefResponse = await this.credDefServiceProxy + .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 credDefResponse; + } catch (error) { + this.logger.error(`Error in creating credential definition : ${JSON.stringify(error)}`); + throw error; + } + } + + async getCredentialDefinitionById(payload: GetCredDefPayload): Promise { + try { + const { credentialDefinitionId, orgId } = payload; + const { agentEndPoint } = await this.credentialDefinitionRepository.getAgentDetailsByOrgId(orgId); + const getAgentDetails = await this.credentialDefinitionRepository.getAgentType(orgId); + const apiKey = ''; + let credDefResponse; + if (1 === getAgentDetails.org_agents[0].orgAgentTypeId) { + const getSchemaPayload = { + credentialDefinitionId, + apiKey, + agentEndPoint, + agentType: 1 + }; + credDefResponse = await this._getCredentialDefinitionById(getSchemaPayload); + } else if (2 === getAgentDetails.org_agents[0].orgAgentTypeId) { + const { tenantId } = await this.credentialDefinitionRepository.getAgentDetailsByOrgId(orgId); + const getSchemaPayload = { + tenantId, + method: 'getCredentialDefinitionById', + payload: { credentialDefinitionId }, + agentType: 2, + agentEndPoint + }; + credDefResponse = await this._getCredentialDefinitionById(getSchemaPayload); + } + if (credDefResponse.response.resolutionMetadata.error) { + throw new NotFoundException(ResponseMessages.credentialDefinition.error.credDefIdNotFound); + } + return credDefResponse; + } catch (error) { + this.logger.error(`Error retrieving credential definition with id ${payload.credentialDefinitionId}`); + throw new RpcException(error.response); + } + } + + async _getCredentialDefinitionById(payload: GetCredDefAgentRedirection): Promise<{ + response: string; + }> { + try { + const pattern = { + cmd: 'agent-get-credential-definition' + }; + const credDefResponse = await this.credDefServiceProxy + .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 credDefResponse; + } catch (error) { + this.logger.error(`Error in creating credential definition : ${JSON.stringify(error)}`); + throw error; + } + } + + async getAllCredDefs(payload: GetAllCredDefsPayload): Promise<{ + totalItems: number; + hasNextPage: boolean; + hasPreviousPage: boolean; + nextPage: number; + previousPage: number; + lastPage: number; + data: { + createDateTime: Date; + createdBy: number; + credentialDefinitionId: string; + tag: string; + schemaLedgerId: string; + schemaId: number; + orgId: number; + revocable: boolean; + }[] + }> { + try { + const { credDefSearchCriteria, orgId } = payload; + const response = await this.credentialDefinitionRepository.getAllCredDefs(credDefSearchCriteria, orgId); + const credDefResponse = { + totalItems: response.length, + hasNextPage: credDefSearchCriteria.pageSize * credDefSearchCriteria.pageNumber < response.length, + hasPreviousPage: 1 < credDefSearchCriteria.pageNumber, + nextPage: credDefSearchCriteria.pageNumber + 1, + previousPage: credDefSearchCriteria.pageNumber - 1, + lastPage: Math.ceil(response.length / credDefSearchCriteria.pageSize), + data: response + }; + + if (0 == response.length) { + throw new NotFoundException(ResponseMessages.credentialDefinition.error.NotFound); + } + return credDefResponse; + + } catch (error) { + this.logger.error(`Error in retrieving credential definitions: ${error}`); + throw new RpcException(error.response); + } + } + +} \ No newline at end of file diff --git a/apps/ledger/src/credential-definition/interfaces/create-credential-definition.interface.ts b/apps/ledger/src/credential-definition/interfaces/create-credential-definition.interface.ts new file mode 100644 index 000000000..ccefdcf37 --- /dev/null +++ b/apps/ledger/src/credential-definition/interfaces/create-credential-definition.interface.ts @@ -0,0 +1,53 @@ +import { SortValue } from '@credebl/enum/enum'; +import { CreateCredentialDefinitionDto } from 'apps/api-gateway/src/credential-definition/dto/create-cred-defs.dto'; +import { IUserRequestInterface } from '.'; + + +export interface GetCredDefPayload { + page?: number; + searchText?: string; + itemsPerPage?: number; + user?: IUserRequestInterface; + orgId?: number; + sortValue?: SortValue; + credDefSortBy?: string; + supportRevocation?: string; + credentialDefinitionId?: string; + orgDid: string; +} + +export interface CreateCredDefPayload { + credDef: CreateCredentialDefinitionDto; + user: IUserRequestInterface; + orgId?: number; +} + +export interface CredDefPayload { + userId?: number, + schemaId?: number; + tag?: string; + issuerId?: string; + credentialDefinitionId?: string; + issuerDid?: string; + schemaLedgerId?: string; + orgId?: number; + createdBy?: number; + autoIssue?: boolean; + revocable?: boolean; + orgDid?: string; +} + +export class GetAllCredDefsDto { + pageSize?: number; + pageNumber?: number; + searchByText?: string; + sorting?: string; + revocable?: boolean; + sortByValue?: string; +} + +export interface GetAllCredDefsPayload { + credDefSearchCriteria: GetAllCredDefsDto, + user: IUserRequestInterface, + orgId: number +} \ No newline at end of file diff --git a/apps/ledger/src/credential-definition/interfaces/credential-definition.interface.ts b/apps/ledger/src/credential-definition/interfaces/credential-definition.interface.ts new file mode 100644 index 000000000..73bbc6c64 --- /dev/null +++ b/apps/ledger/src/credential-definition/interfaces/credential-definition.interface.ts @@ -0,0 +1,31 @@ +export interface CreateCredDefAgentRedirection { + tenantId?: string; + tag?: string; + schemaId?: string; + issuerId?: string; + payload?: ITenantCredDef; + method?: string; + agentType?: number; + apiKey?: string; + agentEndPoint?: string; +} + +export interface ITenantCredDef { + tag: string; + schemaId: string; + issuerId: string; +} + +export interface GetCredDefAgentRedirection { + credentialDefinitionId?: string; + tenantId?: string; + payload?: GetCredDefFromTenantPayload; + apiKey?: string; + agentEndPoint?: string; + agentType?: number; + method?: string; +} + +export interface GetCredDefFromTenantPayload { + credentialDefinitionId: string; +} diff --git a/apps/ledger/src/credential-definition/interfaces/index.ts b/apps/ledger/src/credential-definition/interfaces/index.ts new file mode 100644 index 000000000..95d3e0c0c --- /dev/null +++ b/apps/ledger/src/credential-definition/interfaces/index.ts @@ -0,0 +1,41 @@ +import { UserRoleOrgPermsDto } from '../../schema/dtos/user-role-org-perms.dto'; + +export interface IUserRequestInterface { + userId: number; + email: string; + orgId: number; + agentEndPoint?: string; + apiKey?: string; + tenantId?: number; + tenantName?: string; + tenantOrgId?: number; + userRoleOrgPermissions?: UserRoleOrgPermsDto[]; + orgName?: string; + selectedOrg: ISelectedOrgInterface; +} + +export interface ISelectedOrgInterface { + id: number; + userId: number; + orgRoleId: number; + orgId: number; + orgRole: object; + organisation: IOrganizationInterface; +} + +export interface IOrganizationInterface { + name: string; + description: string; + org_agents: IOrgAgentInterface[] + +} + +export interface IOrgAgentInterface { + orgDid: string; + verkey: string; + agentEndPoint: string; + agentOptions: string; + walletName: string; + agentsTypeId: string; + orgId: string; +} \ No newline at end of file diff --git a/apps/ledger/src/credential-definition/repositories/credential-definition.repository.ts b/apps/ledger/src/credential-definition/repositories/credential-definition.repository.ts new file mode 100644 index 000000000..03690982e --- /dev/null +++ b/apps/ledger/src/credential-definition/repositories/credential-definition.repository.ts @@ -0,0 +1,155 @@ +/* eslint-disable camelcase */ +import { CredDefPayload, GetAllCredDefsDto } from '../interfaces/create-credential-definition.interface'; +import { PrismaService } from '@credebl/prisma-service'; +import { credential_definition, org_agents, org_agents_type, organisation, schema } from '@prisma/client'; +import { Injectable, Logger } from '@nestjs/common'; +import { ResponseMessages } from '@credebl/common/response-messages'; + +@Injectable() +export class CredentialDefinitionRepository { + private readonly logger = new Logger('CredentialDefinitionRepository'); + + constructor( + private prisma: PrismaService + ) { } + + async saveCredentialDefinition(credDef: CredDefPayload): Promise { + try { + const dbResult: credential_definition = await this.getByAttribute( + credDef.schemaLedgerId, + credDef.tag + ); + if (!dbResult) { + const saveResult = await this.prisma.credential_definition.create({ + data: { + schemaLedgerId: credDef.schemaLedgerId, + tag: credDef.tag, + credentialDefinitionId: credDef.credentialDefinitionId, + revocable: credDef.revocable, + createdBy: credDef.userId, + orgId: credDef.orgId, + schemaId: credDef.schemaId + } + }); + return saveResult; + } + } catch (error) { + this.logger.error( + `${ResponseMessages.credentialDefinition.error.NotSaved}: ${error.message} ` + ); + throw error; + } + } + + async getSchemaById(schemaLedgerId: string): Promise { + try { + const response = await this.prisma.schema.findFirst({ where: { schemaLedgerId } }); + return response; + } catch (error) { + this.logger.error( + `${ResponseMessages.credentialDefinition.error.NotSaved}: ${error.message} ` + ); + throw error; + } + } + + async getByAttribute(schema: string, tag: string): Promise { + try { + const response = await this.prisma.credential_definition.findFirst({ where: { schemaLedgerId: schema, tag: { contains: tag, mode: 'insensitive' } } }); + return response; + } catch (error) { + this.logger.error(`${ResponseMessages.credentialDefinition.error.NotFound}: ${error}`); + } + } + + async getAllCredDefs(credDefSearchCriteria: GetAllCredDefsDto, orgId: number): Promise<{ + createDateTime: Date; + createdBy: number; + credentialDefinitionId: string; + tag: string; + schemaLedgerId: string; + schemaId: number; + orgId: number; + revocable: boolean; + }[]> { + try { + const credDefResult = await this.prisma.credential_definition.findMany({ + where: { + orgId, + OR: [ + { tag: { contains: credDefSearchCriteria.searchByText, mode: 'insensitive' } }, + { credentialDefinitionId: { contains: credDefSearchCriteria.searchByText, mode: 'insensitive' } }, + { schemaLedgerId: { contains: credDefSearchCriteria.searchByText, mode: 'insensitive' } } + ] + }, + select: { + createDateTime: true, + tag: true, + schemaId: true, + orgId: true, + schemaLedgerId: true, + createdBy: true, + credentialDefinitionId: true, + revocable: true + }, + orderBy: { + [credDefSearchCriteria.sorting]: 'DESC' === credDefSearchCriteria.sortByValue ? 'desc' : 'ASC' === credDefSearchCriteria.sortByValue ? 'asc' : 'desc' + }, + take: credDefSearchCriteria.pageSize, + skip: (credDefSearchCriteria.pageNumber - 1) * credDefSearchCriteria.pageSize + }); + return credDefResult; + } catch (error) { + this.logger.error(`Error in getting credential definitions: ${error}`); + throw error; + } + } + + async getAgentDetailsByOrgId(orgId: number): Promise<{ + orgDid: string; + agentEndPoint: string; + tenantId: string + }> { + try { + const schemasResult = await this.prisma.org_agents.findFirst({ + where: { + orgId + }, + select: { + orgDid: true, + agentEndPoint: true, + tenantId: true + } + }); + return schemasResult; + } catch (error) { + this.logger.error(`Error in getting agent DID: ${error}`); + throw error; + } + } + + async getAgentType(orgId: number): Promise { + try { + const agentDetails = await this.prisma.organisation.findUnique({ + where: { + id: orgId + }, + include: { + org_agents: { + include: { + org_agent_type: true + } + } + } + }); + return agentDetails; + } catch (error) { + this.logger.error(`Error in getting agent type: ${error}`); + throw error; + } + } +} \ No newline at end of file diff --git a/apps/ledger/src/ledger.controller.spec.ts b/apps/ledger/src/ledger.controller.spec.ts new file mode 100644 index 000000000..a40191922 --- /dev/null +++ b/apps/ledger/src/ledger.controller.spec.ts @@ -0,0 +1,22 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { LedgerServiceController } from './ledger.controller'; +import { LedgerServiceService } from './ledger.service'; + +describe('LedgerServiceController', () => { + let ledgerServiceController: LedgerServiceController; + + beforeEach(async () => { + const app: TestingModule = await Test.createTestingModule({ + controllers: [LedgerServiceController], + providers: [LedgerServiceService] + }).compile(); + + ledgerServiceController = app.get(LedgerServiceController); + }); + + describe('root', () => { + it('should return "Hello World!"', () => { + expect(ledgerServiceController.getHello()).toBe('Hello World!'); + }); + }); +}); diff --git a/apps/ledger/src/ledger.controller.ts b/apps/ledger/src/ledger.controller.ts new file mode 100644 index 000000000..10ec63cd2 --- /dev/null +++ b/apps/ledger/src/ledger.controller.ts @@ -0,0 +1,8 @@ +import { Controller } from '@nestjs/common'; +import { LedgerService } from './ledger.service'; + +@Controller() +export class LedgerController { + constructor(private readonly ledgerService: LedgerService) {} + +} diff --git a/apps/ledger/src/ledger.module.ts b/apps/ledger/src/ledger.module.ts new file mode 100644 index 000000000..3728649a8 --- /dev/null +++ b/apps/ledger/src/ledger.module.ts @@ -0,0 +1,25 @@ +import { Module } from '@nestjs/common'; +import { LedgerController } from './ledger.controller'; +import { LedgerService } from './ledger.service'; +import { SchemaModule } from './schema/schema.module'; +import { PrismaService } from '@credebl/prisma-service'; +import { CredentialDefinitionModule } from './credential-definition/credential-definition.module'; +import { ClientsModule, Transport } from '@nestjs/microservices'; + +@Module({ + imports: [ + ClientsModule.register([ + { + name: 'NATS_CLIENT', + transport: Transport.NATS, + options: { + servers: [`${process.env.NATS_URL}`] + } + } + ]), + SchemaModule, CredentialDefinitionModule +], + controllers: [LedgerController], + providers: [LedgerService, PrismaService] +}) +export class LedgerModule { } diff --git a/apps/ledger/src/ledger.service.ts b/apps/ledger/src/ledger.service.ts new file mode 100644 index 000000000..60f32c21a --- /dev/null +++ b/apps/ledger/src/ledger.service.ts @@ -0,0 +1,5 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class LedgerService { +} diff --git a/apps/ledger/src/main.ts b/apps/ledger/src/main.ts new file mode 100644 index 000000000..c92cc071b --- /dev/null +++ b/apps/ledger/src/main.ts @@ -0,0 +1,20 @@ +import { NestFactory } from '@nestjs/core'; +import { LedgerModule } from './ledger.module'; +import { HttpExceptionFilter } from 'libs/http-exception.filter'; +import { Logger } from '@nestjs/common'; +import { MicroserviceOptions, Transport } from '@nestjs/microservices'; + + +async function bootstrap(): Promise { + const app = await NestFactory.createMicroservice(LedgerModule, { + transport: Transport.NATS, + options: { + servers: [`${process.env.NATS_URL}`] + } + }); + app.useGlobalFilters(new HttpExceptionFilter()); + + await app.listen(); + Logger.log('Ladger-Service Microservice is listening to NATS '); +} +bootstrap(); diff --git a/apps/ledger/src/schema/dtos/user-role-org-perms.dto.ts b/apps/ledger/src/schema/dtos/user-role-org-perms.dto.ts new file mode 100644 index 000000000..bcadd2a1a --- /dev/null +++ b/apps/ledger/src/schema/dtos/user-role-org-perms.dto.ts @@ -0,0 +1,27 @@ + +export class UserRoleOrgPermsDto { + id?: number; + role?: userRoleDto; + organization?: userOrgDto; + userRoleOrgPermissions?: unknown; + } + + export class userRoleDto { + id: number; + name: string; + permissions: string[]; + + } + + export class OrgRole { + id: number; + } + + export class userOrgDto { + id?: number; + orgName?: string; + orgRole?: OrgRole; + agentEndPoint?: string; + apiKey?: string; + } + \ No newline at end of file diff --git a/apps/ledger/src/schema/interfaces/schema-payload.interface.ts b/apps/ledger/src/schema/interfaces/schema-payload.interface.ts new file mode 100644 index 000000000..86633aed6 --- /dev/null +++ b/apps/ledger/src/schema/interfaces/schema-payload.interface.ts @@ -0,0 +1,59 @@ +import { SortValue } from '@credebl/enum/enum'; +import { IUserRequestInterface } from './schema.interface'; + +export interface ISchema { + schema?: ISchemaPayload; + user?: IUserRequestInterface; + createdBy?: number; + issuerId?: string; + changedBy?: number; + ledgerId?: number; + orgId?: number; + onLedgerStatus?: string; + credDefSortBy?: string; + supportRevocation?: string; + schemaId?: string; + createTransactionForEndorser?: boolean; + transactionId?: string; + endorserWriteTxn?: string; + orgDid?: string; +} + +export interface ISchemaPayload { + schemaVersion: string; + schemaName: string; + orgDid?: string; + attributes: string[]; + issuerId?: string; + onLedgerStatus?: string; + id?: string; + user?: IUserRequestInterface; + page?: number; + searchText?: string + itemsPerPage?: number; + sortValue?: SortValue; + schemaSortBy?: string; +} + +export interface ISchemaSearchInterface { + schemaSearchCriteria: ISchemaSearchCriteria, + user: IUserRequestInterface, + orgId: number +} + +export interface ISchemaSearchCriteria { + pageNumber: number; + pageSize: number; + sorting: string; + sortByValue: string; + searchByText: string; + user: IUserRequestInterface +} + +export interface ISchemaCredDeffSearchInterface { + schemaId: string; + schemaSearchCriteria?: ISchemaSearchCriteria, + user: IUserRequestInterface, + orgId: number +} + diff --git a/apps/ledger/src/schema/interfaces/schema.interface.ts b/apps/ledger/src/schema/interfaces/schema.interface.ts new file mode 100644 index 000000000..fc777ce4c --- /dev/null +++ b/apps/ledger/src/schema/interfaces/schema.interface.ts @@ -0,0 +1,41 @@ +import { UserRoleOrgPermsDto } from '../dtos/user-role-org-perms.dto'; + +export interface IUserRequestInterface { + userId: number; + email: string; + orgId: number; + agentEndPoint?: string; + apiKey?: string; + tenantId?: number; + tenantName?: string; + tenantOrgId?: number; + userRoleOrgPermissions?: UserRoleOrgPermsDto[]; + orgName?: string; + selectedOrg: ISelectedOrgInterface; +} + +export interface ISelectedOrgInterface { + id: number; + userId: number; + orgRoleId: number; + orgId: number; + orgRole: object; + organisation: IOrganizationInterface; +} + +export interface IOrganizationInterface { + name: string; + description: string; + org_agents: IOrgAgentInterface[] + +} + +export interface IOrgAgentInterface { + orgDid: string; + verkey: string; + agentEndPoint: string; + agentOptions: string; + walletName: string; + agentsTypeId: string; + orgId: string; +} \ No newline at end of file diff --git a/apps/ledger/src/schema/repositories/schema.repository.ts b/apps/ledger/src/schema/repositories/schema.repository.ts new file mode 100644 index 000000000..e70918ff2 --- /dev/null +++ b/apps/ledger/src/schema/repositories/schema.repository.ts @@ -0,0 +1,196 @@ +/* eslint-disable camelcase */ +import { BadRequestException, Injectable, Logger } from '@nestjs/common'; +import { PrismaService } from '@credebl/prisma-service'; +import { org_agents, org_agents_type, organisation, schema } from '@prisma/client'; +import { ISchema, ISchemaSearchCriteria } from '../interfaces/schema-payload.interface'; +import { ResponseMessages } from '@credebl/common/response-messages'; + +@Injectable() +export class SchemaRepository { + private readonly logger = new Logger('SchemaRepository'); + + 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 schemaLength = 0; + if (schema.length !== schemaLength) { + throw new BadRequestException( + ResponseMessages.schema.error.exists + ); + } + const saveResult = await this.prisma.schema.create({ + data: { + name: schemaResult.schema.schemaName, + version: schemaResult.schema.schemaVersion, + attributes: schemaResult.schema.attributes, + schemaLedgerId: schemaResult.schema.id, + issuerId: schemaResult.issuerId, + createdBy: schemaResult.createdBy, + lastChangedBy: schemaResult.changedBy, + publisherDid: schemaResult.issuerId.split(':')[3], + orgId: schemaResult.orgId, + ledgerId: schemaResult.ledgerId + } + }); + return saveResult; + } + } catch (error) { + this.logger.error(`Error in saving schema repository: ${error.message} `); + throw error; + } + } + + async schemaExists(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; + } + } + + async getSchemas(payload: ISchemaSearchCriteria, orgId: number): Promise<{ + createDateTime: Date; + createdBy: number; + name: string; + version: string; + attributes: string[]; + schemaLedgerId: string; + publisherDid: string; + orgId: number; + issuerId: string; + }[]> { + try { + const schemasResult = await this.prisma.schema.findMany({ + where: { + orgId, + OR: [ + { name: { contains: payload.searchByText, mode: 'insensitive' } }, + { version: { contains: payload.searchByText, mode: 'insensitive' } }, + { schemaLedgerId: { contains: payload.searchByText, mode: 'insensitive' } }, + { issuerId: { contains: payload.searchByText, mode: 'insensitive' } } + ] + }, + select: { + createDateTime: true, + name: true, + version: true, + attributes: true, + schemaLedgerId: true, + createdBy: true, + publisherDid: true, + orgId: true, + issuerId: true + }, + orderBy: { + [payload.sorting]: 'DESC' === payload.sortByValue ? 'desc' : 'ASC' === payload.sortByValue ? 'asc' : 'desc' + }, + take: payload.pageSize, + skip: (payload.pageNumber - 1) * payload.pageSize + }); + return schemasResult; + } catch (error) { + this.logger.error(`Error in getting schemas: ${error}`); + throw error; + } + } + + async getAgentDetailsByOrgId(orgId: number): Promise<{ + orgDid: string; + agentEndPoint: string; + tenantId: string + }> { + try { + const schemasResult = await this.prisma.org_agents.findFirst({ + where: { + orgId + }, + select: { + orgDid: true, + agentEndPoint: true, + tenantId: true + } + }); + return schemasResult; + } catch (error) { + this.logger.error(`Error in getting agent DID: ${error}`); + throw error; + } + } + + async getAgentType(orgId: number): Promise { + try { + const agentDetails = await this.prisma.organisation.findUnique({ + where: { + id: orgId + }, + include: { + org_agents: { + include: { + org_agent_type: true + } + } + } + }); + return agentDetails; + } catch (error) { + this.logger.error(`Error in getting agent type: ${error}`); + throw error; + } + } + + async getSchemasCredDeffList(payload: ISchemaSearchCriteria, orgId: number, schemaId:string): Promise<{ + tag: string; + credentialDefinitionId: string; + schemaLedgerId: string; + revocable: boolean; +}[]> { + try { + const schemasResult = await this.prisma.credential_definition.findMany({ + where: { + AND:[ + {orgId}, + {schemaLedgerId:schemaId} + ] + }, + select: { + tag: true, + credentialDefinitionId: true, + schemaLedgerId: true, + revocable: true, + createDateTime: true + }, + orderBy: { + [payload.sorting]: 'DESC' === payload.sortByValue ? 'desc' : 'ASC' === payload.sortByValue ? 'asc' : 'desc' + } + }); + return schemasResult; + } catch (error) { + this.logger.error(`Error in getting agent DID: ${error}`); + throw error; + } +} +} \ No newline at end of file diff --git a/apps/ledger/src/schema/schema.controller.ts b/apps/ledger/src/schema/schema.controller.ts new file mode 100644 index 000000000..50ca32140 --- /dev/null +++ b/apps/ledger/src/schema/schema.controller.ts @@ -0,0 +1,67 @@ +import { Controller } from '@nestjs/common'; +import { SchemaService } from './schema.service'; +import { MessagePattern } from '@nestjs/microservices'; +import { ISchema, ISchemaCredDeffSearchInterface, ISchemaSearchInterface } from './interfaces/schema-payload.interface'; +import { schema } from '@prisma/client'; + + +@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: ISchemaSearchInterface): Promise<{ + totalItems: number; + hasNextPage: boolean; + hasPreviousPage: boolean; + nextPage: number; + previousPage: number; + lastPage: number; + data: { + createDateTime: Date; + createdBy: number; + name: string; + version: string; + attributes: string[]; + schemaLedgerId: string; + publisherDid: string; + orgId: number; + issuerId: string; + }[]; + }> { + const { schemaSearchCriteria, user, orgId } = schemaSearch; + return this.schemaService.getSchemas(schemaSearchCriteria, user, orgId); + } + + @MessagePattern({ cmd: 'get-cred-deff-list-by-schemas-id' }) + async getcredDeffListBySchemaId(payload: ISchemaCredDeffSearchInterface): Promise<{ + totalItems: number; + hasNextPage: boolean; + hasPreviousPage: boolean; + nextPage: number; + previousPage: number; + lastPage: number; + data: { + tag: string; + credentialDefinitionId: string; + schemaLedgerId: string; + revocable: boolean; + }[]; + }> { + const {schemaId, schemaSearchCriteria, user, orgId } = payload; + return this.schemaService.getcredDeffListBySchemaId(schemaId, schemaSearchCriteria, user, orgId); + } + +} diff --git a/apps/ledger/src/schema/schema.interface.ts b/apps/ledger/src/schema/schema.interface.ts new file mode 100644 index 000000000..555482acc --- /dev/null +++ b/apps/ledger/src/schema/schema.interface.ts @@ -0,0 +1,41 @@ +import { IUserRequestInterface } from "./interfaces/schema.interface"; + +export interface SchemaSearchCriteria { + schemaLedgerId: string; + credentialDefinitionId: string; + user : IUserRequestInterface +} + +export interface CreateSchemaAgentRedirection { + tenantId?: string; + attributes?: string[]; + version?: string; + name?: string; + issuerId?: string; + payload?: ITenantSchemaDto; + method?: string; + agentType?: number; + apiKey?: string; + agentEndPoint?: string; +} + +export interface ITenantSchemaDto { + attributes: string[]; + version: string; + name: string; + issuerId: string; +} + +export interface GetSchemaAgentRedirection { + schemaId?: string; + tenantId?: string; + payload?: GetSchemaFromTenantPayload; + apiKey?: string; + agentEndPoint?: string; + agentType?: number; + method?: string; +} + +export interface GetSchemaFromTenantPayload { + schemaId: string; +} diff --git a/apps/ledger/src/schema/schema.module.ts b/apps/ledger/src/schema/schema.module.ts new file mode 100644 index 000000000..d4e409750 --- /dev/null +++ b/apps/ledger/src/schema/schema.module.ts @@ -0,0 +1,34 @@ +import { ClientsModule, Transport } from '@nestjs/microservices'; +import { Logger, Module } from '@nestjs/common'; + +import { CommonModule } from '@credebl/common'; +import { SchemaController } from './schema.controller'; +import { SchemaRepository } from './repositories/schema.repository'; +import { SchemaService } from './schema.service'; +import { HttpModule } from '@nestjs/axios'; +import { PrismaService } from '@credebl/prisma-service'; + +@Module({ + imports: [ + ClientsModule.register([ + { + name: 'NATS_CLIENT', + transport: Transport.NATS, + options: { + servers: [`${process.env.NATS_URL}`] + } + } + ]), + + HttpModule, + CommonModule + ], + providers: [ + SchemaService, + SchemaRepository, + Logger, + PrismaService + ], + controllers: [SchemaController] +}) +export class SchemaModule { } diff --git a/apps/ledger/src/schema/schema.service.ts b/apps/ledger/src/schema/schema.service.ts new file mode 100644 index 000000000..05219cd86 --- /dev/null +++ b/apps/ledger/src/schema/schema.service.ts @@ -0,0 +1,353 @@ +/* eslint-disable camelcase */ +import { + BadRequestException, + HttpException, + Inject, + ConflictException, + Injectable, + NotAcceptableException, NotFoundException +} from '@nestjs/common'; +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, ISchemaPayload, ISchemaSearchCriteria } from './interfaces/schema-payload.interface'; +import { ResponseMessages } from '@credebl/common/response-messages'; +import { IUserRequestInterface } from './interfaces/schema.interface'; +import { CreateSchemaAgentRedirection, GetSchemaAgentRedirection } from './schema.interface'; +import { map } from 'rxjs/operators'; + +@Injectable() +export class SchemaService extends BaseService { + constructor( + private readonly schemaRepository: SchemaRepository, + @Inject('NATS_CLIENT') private readonly schemaServiceProxy: ClientProxy + ) { + super('SchemaService'); + } + + async createSchema( + schema: ISchemaPayload, + user: IUserRequestInterface, + orgId: number + ): Promise { + const apiKey = ''; + const { userId } = user.selectedOrg; + try { + const schemaExists = await this.schemaRepository.schemaExists( + schema.schemaName, + schema.schemaVersion + ); + + if (0 !== schemaExists.length) { + this.logger.error(ResponseMessages.schema.error.exists); + throw new ConflictException(ResponseMessages.schema.error.exists); + } + + if (null !== schema || schema !== undefined) { + const schemaVersionIndexOf = -1; + if ( + isNaN(parseFloat(schema.schemaVersion)) || + schema.schemaVersion.toString().indexOf('.') === + schemaVersionIndexOf + ) { + throw new NotAcceptableException( + ResponseMessages.schema.error.invalidVersion + ); + } + + const schemaAttributeLength = 0; + if (schema.attributes.length === schemaAttributeLength) { + throw new NotAcceptableException( + ResponseMessages.schema.error.insufficientAttributes + ); + } else if (schema.attributes.length > schemaAttributeLength) { + const schemaAttibute: string[] = schema.attributes; + const findDuplicates: boolean = + new Set(schemaAttibute).size !== schemaAttibute.length; + if (true === findDuplicates) { + throw new NotAcceptableException( + ResponseMessages.schema.error.invalidAttributes + ); + } + schema.schemaName = schema.schemaName.trim(); + + const { agentEndPoint, orgDid } = await this.schemaRepository.getAgentDetailsByOrgId(orgId); + const getAgentDetails = await this.schemaRepository.getAgentType(orgId); + // eslint-disable-next-line yoda + const did = schema.orgDid?.split(':').length >= 4 ? schema.orgDid : orgDid; + + + let schemaResponseFromAgentService; + if (1 === getAgentDetails.org_agents[0].orgAgentTypeId) { + const issuerId = did; + const schemaPayload = { + attributes: schema.attributes, + version: schema.schemaVersion, + name: schema.schemaName, + issuerId, + agentEndPoint, + apiKey, + agentType: 1 + }; + schemaResponseFromAgentService = await this._createSchema(schemaPayload); + + } else if (2 === getAgentDetails.org_agents[0].orgAgentTypeId) { + const { tenantId } = await this.schemaRepository.getAgentDetailsByOrgId(orgId); + + const schemaPayload = { + tenantId, + method: 'registerSchema', + payload: { + attributes: schema.attributes, + version: schema.schemaVersion, + name: schema.schemaName, + issuerId: did + }, + agentEndPoint, + apiKey, + agentType: 2 + }; + schemaResponseFromAgentService = await this._createSchema(schemaPayload); + } + + const responseObj = JSON.parse(JSON.stringify(schemaResponseFromAgentService.response)); + + const schemaDetails: ISchema = { + schema: { schemaName: '', attributes: [], schemaVersion: '', id: '' }, + createdBy: 0, + issuerId: '', + onLedgerStatus: 'Submitted on ledger', + orgId, + ledgerId: 0 + }; + + if ('finished' === responseObj.schema.state) { + schemaDetails.schema.schemaName = responseObj.schema.schema.name; + schemaDetails.schema.attributes = responseObj.schema.schema.attrNames; + schemaDetails.schema.schemaVersion = responseObj.schema.schema.version; + schemaDetails.createdBy = userId; + schemaDetails.schema.id = responseObj.schema.schemaId; + schemaDetails.changedBy = userId; + schemaDetails.orgId = Number(orgId); + schemaDetails.issuerId = responseObj.schema.schema.issuerId; + const saveResponse = this.schemaRepository.saveSchema( + schemaDetails + ); + return saveResponse; + + } else if ('finished' === responseObj.state) { + schemaDetails.schema.schemaName = responseObj.schema.name; + schemaDetails.schema.attributes = responseObj.schema.attrNames; + schemaDetails.schema.schemaVersion = responseObj.schema.version; + schemaDetails.createdBy = userId; + schemaDetails.schema.id = responseObj.schemaId; + schemaDetails.changedBy = userId; + schemaDetails.orgId = Number(orgId); + schemaDetails.issuerId = responseObj.schema.issuerId; + const saveResponse = this.schemaRepository.saveSchema( + schemaDetails + ); + return saveResponse; + } else { + throw new NotFoundException(ResponseMessages.schema.error.notCreated); + } + } else { + throw new RpcException( + new BadRequestException( + ResponseMessages.schema.error.emptyData + ) + ); + } + } else { + throw new RpcException( + new BadRequestException( + ResponseMessages.schema.error.emptyData + ) + ); + } + + } catch (error) { + this.logger.error( + `[createSchema] - outer Error: ${JSON.stringify(error)}` + ); + throw new RpcException(error.response); + } + } + + async _createSchema(payload: CreateSchemaAgentRedirection): Promise<{ + response: string; + }> { + try { + const pattern = { + cmd: 'agent-create-schema' + }; + const schemaResponse = await this.schemaServiceProxy + .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 schemaResponse; + } catch (error) { + this.logger.error(`Error in creating schema : ${JSON.stringify(error)}`); + throw error; + } + } + + + async getSchemaById(schemaId: string, orgId: number): Promise { + try { + const { agentEndPoint } = await this.schemaRepository.getAgentDetailsByOrgId(orgId); + const getAgentDetails = await this.schemaRepository.getAgentType(orgId); + const apiKey = ''; + let schemaResponse; + if (1 === getAgentDetails.org_agents[0].orgAgentTypeId) { + const getSchemaPayload = { + schemaId, + apiKey, + agentEndPoint, + agentType: 1 + }; + schemaResponse = await this._getSchemaById(getSchemaPayload); + } else if (2 === getAgentDetails.org_agents[0].orgAgentTypeId) { + const { tenantId } = await this.schemaRepository.getAgentDetailsByOrgId(orgId); + const getSchemaPayload = { + tenantId, + method: 'getSchemaById', + payload: { schemaId }, + agentType: 2, + agentEndPoint + }; + schemaResponse = await this._getSchemaById(getSchemaPayload); + } + + return schemaResponse; + + } catch (error) { + this.logger.error(`Error in getting schema by id: ${error}`); + throw new RpcException(error.response); + } + } + + async _getSchemaById(payload: GetSchemaAgentRedirection): Promise<{ response: string }> { + try { + const pattern = { + cmd: 'agent-get-schema' + }; + const schemaResponse = await this.schemaServiceProxy + .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 schemaResponse; + } catch (error) { + this.logger.error(`Error in getting schema : ${JSON.stringify(error)}`); + throw error; + } + } + + async getSchemas(schemaSearchCriteria: ISchemaSearchCriteria, user: IUserRequestInterface, orgId: number): Promise<{ + totalItems: number; + hasNextPage: boolean; + hasPreviousPage: boolean; + nextPage: number; + previousPage: number; + lastPage: number; + data: { + createDateTime: Date; + createdBy: number; + name: string; + version: string; + attributes: string[]; + schemaLedgerId: string; + publisherDid: string; + orgId: number; + issuerId: string; + }[]; + }> { + try { + const response = await this.schemaRepository.getSchemas(schemaSearchCriteria, orgId); + const schemasResponse = { + totalItems: response.length, + hasNextPage: schemaSearchCriteria.pageSize * schemaSearchCriteria.pageNumber < response.length, + hasPreviousPage: 1 < schemaSearchCriteria.pageNumber, + nextPage: schemaSearchCriteria.pageNumber + 1, + previousPage: schemaSearchCriteria.pageNumber - 1, + lastPage: Math.ceil(response.length / schemaSearchCriteria.pageSize), + data: response + }; + + if (0 !== response.length) { + return schemasResponse; + } else { + throw new NotFoundException(ResponseMessages.schema.error.notFound); + } + + + } catch (error) { + this.logger.error(`Error in retrieving schemas: ${error}`); + throw new RpcException(error.response); + } + } + + async getcredDeffListBySchemaId(schemaId: string, schemaSearchCriteria: ISchemaSearchCriteria, user: IUserRequestInterface, orgId: number): Promise<{ + totalItems: number; + hasNextPage: boolean; + hasPreviousPage: boolean; + nextPage: number; + previousPage: number; + lastPage: number; + data: { + tag: string; + credentialDefinitionId: string; + schemaLedgerId: string; + revocable: boolean; + }[]; + }> { + try { + const response = await this.schemaRepository.getSchemasCredDeffList(schemaSearchCriteria, orgId, schemaId); + const schemasResponse = { + totalItems: response.length, + hasNextPage: schemaSearchCriteria.pageSize * schemaSearchCriteria.pageNumber < response.length, + hasPreviousPage: 1 < schemaSearchCriteria.pageNumber, + nextPage: schemaSearchCriteria.pageNumber + 1, + previousPage: schemaSearchCriteria.pageNumber - 1, + lastPage: Math.ceil(response.length / schemaSearchCriteria.pageSize), + data: response + }; + + if (0 !== response.length) { + return schemasResponse; + } else { + throw new NotFoundException(ResponseMessages.schema.error.credentialDefinitionNotFound); + } + + + } catch (error) { + this.logger.error(`Error in retrieving credential definition: ${error}`); + throw new RpcException(error.response); + } + } +} diff --git a/apps/ledger/tsconfig.app.json b/apps/ledger/tsconfig.app.json new file mode 100644 index 000000000..3a817ff09 --- /dev/null +++ b/apps/ledger/tsconfig.app.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "declaration": false, + "outDir": "../../dist/apps/ledger" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] +} diff --git a/apps/organization/Dockerfile b/apps/organization/Dockerfile new file mode 100644 index 000000000..58c6c8ec1 --- /dev/null +++ b/apps/organization/Dockerfile @@ -0,0 +1,43 @@ +# Stage 1: Build the application +FROM node:16-alpine as build + +# Set the working directory +WORKDIR /app + +# Copy package.json and package-lock.json +COPY package.json ./ +#COPY package-lock.json ./ + +# Install dependencies +RUN npm i + +# Copy the rest of the application code +COPY . . +CMD ["sh", "-c", "cd libs/prisma-service && npx prisma generate"] + +# Build the user service +RUN npm run build organization + +# Stage 2: Create the final image +FROM node:16-alpine + +# Set the working directory +WORKDIR /app + +# Copy the compiled code from the build stage +COPY --from=build /app/dist/apps/organization/ ./dist/apps/organization/ + +# 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 + +# Install production dependencies for the final image +#RUN npm i --only=production + +# Set the command to run the microservice +CMD ["sh", "-c", "cd libs/prisma-service && npx prisma generate && cd ../.. && node dist/apps/organization/main.js"] + +# docker build -t organization -f apps/organization/Dockerfile . +# docker run -d --env-file .env --name issuance docker.io/library/organization +# docker logs -f organization diff --git a/apps/organization/dtos/create-organization.dto.ts b/apps/organization/dtos/create-organization.dto.ts new file mode 100644 index 000000000..c2fd5249e --- /dev/null +++ b/apps/organization/dtos/create-organization.dto.ts @@ -0,0 +1,15 @@ +import { ApiExtraModels } from '@nestjs/swagger'; + +@ApiExtraModels() +export class CreateOrganizationDto { + name?: string; + description?: string; + logo?: string; + website?: string; +} + +export class CreateUserRoleOrgDto { + orgRoleId: number; + userId: number; + organisationId: number; +} \ No newline at end of file diff --git a/apps/organization/dtos/send-invitation.dto.ts b/apps/organization/dtos/send-invitation.dto.ts new file mode 100644 index 000000000..c80708b8c --- /dev/null +++ b/apps/organization/dtos/send-invitation.dto.ts @@ -0,0 +1,13 @@ +import { ApiExtraModels } from '@nestjs/swagger'; + +@ApiExtraModels() +export class SendInvitationDto { + email: string; + orgRoleId: number[]; +} + +@ApiExtraModels() +export class BulkSendInvitationDto { + invitations: SendInvitationDto[]; + orgId: number; +} \ No newline at end of file diff --git a/apps/organization/dtos/update-invitation.dt.ts b/apps/organization/dtos/update-invitation.dt.ts new file mode 100644 index 000000000..b5a2aa78c --- /dev/null +++ b/apps/organization/dtos/update-invitation.dt.ts @@ -0,0 +1,7 @@ +export class UpdateInvitationDto { + invitationId: number; + orgId: number; + status: string; + userId: number; + email: string; +} \ No newline at end of file diff --git a/apps/organization/dtos/verify-email-token.dto.ts b/apps/organization/dtos/verify-email-token.dto.ts new file mode 100644 index 000000000..27ca0154f --- /dev/null +++ b/apps/organization/dtos/verify-email-token.dto.ts @@ -0,0 +1,8 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class VerifyEmailTokenDto { + @ApiProperty() + email: string; + @ApiProperty() + verificationCode: string; +} \ No newline at end of file diff --git a/apps/organization/interfaces/organization.interface.ts b/apps/organization/interfaces/organization.interface.ts new file mode 100644 index 000000000..5a1d9667f --- /dev/null +++ b/apps/organization/interfaces/organization.interface.ts @@ -0,0 +1,21 @@ +export interface IUserOrgRoles { + id: number + userId: number + orgRoleId: number + orgId: number | null, + orgRole: OrgRole +} + +export interface OrgRole { + id: number + name: string + description: string +} + +export interface IUpdateOrganization { + name: string; + description?: string; + orgId: string; + logo?: string; + website?: string; +} \ No newline at end of file diff --git a/apps/organization/repositories/organization.repository.ts b/apps/organization/repositories/organization.repository.ts new file mode 100644 index 000000000..e4ce211c9 --- /dev/null +++ b/apps/organization/repositories/organization.repository.ts @@ -0,0 +1,397 @@ +/* eslint-disable prefer-destructuring */ +/* eslint-disable camelcase */ + +import { Injectable, Logger } from '@nestjs/common'; +// eslint-disable-next-line camelcase +import { org_invitations, user_org_roles } from '@prisma/client'; + +import { CreateOrganizationDto } from '../dtos/create-organization.dto'; +import { InternalServerErrorException } from '@nestjs/common'; +import { Invitation } from '@credebl/enum/enum'; +import { PrismaService } from '@credebl/prisma-service'; +import { UserOrgRolesService } from '@credebl/user-org-roles'; +import { organisation } from '@prisma/client'; +import { IUpdateOrganization } from '../interfaces/organization.interface'; + +@Injectable() +export class OrganizationRepository { + constructor( + private readonly prisma: PrismaService, + private readonly logger: Logger, + private readonly userOrgRoleService: UserOrgRolesService + ) {} + + /** + * + * @param name + * @returns Organization exist details + */ + + async checkOrganizationNameExist(name: string): Promise { + try { + return this.prisma.organisation.findFirst({ + where: { + name + } + }); + } catch (error) { + this.logger.error(`error: ${JSON.stringify(error)}`); + throw new InternalServerErrorException(error); + } + } + + /** + * + * @Body createOrgDtp + * @returns create Organization + */ + + async createOrganization(createOrgDto: CreateOrganizationDto): Promise { + try { + return this.prisma.organisation.create({ + data: { + name: createOrgDto.name, + logoUrl: createOrgDto.logo, + description: createOrgDto.description, + website: createOrgDto.website + } + }); + } catch (error) { + this.logger.error(`error: ${JSON.stringify(error)}`); + throw new InternalServerErrorException(error); + } + } + + /** + * + * @Body updateOrgDt0 + * @returns update Organization + */ + + async updateOrganization(updateOrgDto: IUpdateOrganization): Promise { + try { + return this.prisma.organisation.update({ + where: { + id: Number(updateOrgDto.orgId) + }, + data: { + name: updateOrgDto.name, + logoUrl: updateOrgDto.logo, + description: updateOrgDto.description, + website: updateOrgDto.website + } + }); + + } catch (error) { + this.logger.error(`error: ${JSON.stringify(error)}`); + throw new InternalServerErrorException(error); + } + } + + + /** + * + * @Body userOrgRoleDto + * @returns create userOrgRole + */ + + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types + async createUserOrgRole(userOrgRoleDto): Promise { + try { + return this.prisma.user_org_roles.create({ + data: { + userId: userOrgRoleDto.userId, + orgRoleId: userOrgRoleDto.orgRoleId, + orgId: userOrgRoleDto.orgId + } + }); + } catch (error) { + this.logger.error(`error: ${JSON.stringify(error)}`); + throw new InternalServerErrorException(error); + } + } + + /** + * + * @Body sendInvitationDto + * @returns orgInvitaionDetails + */ + + async createSendInvitation( + email: string, + orgId: number, + userId: number, + orgRoleId: number[] + ): Promise { + try { + return this.prisma.org_invitations.create({ + data: { + email, + user: { connect: { id: userId } }, + organisation: { connect: { id: orgId } }, + orgRoles: orgRoleId, + status: Invitation.PENDING + } + }); + } catch (error) { + this.logger.error(`error: ${JSON.stringify(error)}`); + throw new InternalServerErrorException(error); + } + } + + /** + * + * @param orgId + * @returns OrganizationDetails + */ + + async getOrganizationDetails(orgId: number): Promise { + try { + return this.prisma.organisation.findFirst({ + where: { + id: orgId + } + }); + } catch (error) { + this.logger.error(`error: ${JSON.stringify(error)}`); + throw new InternalServerErrorException(error); + } + } + + async getAllOrgInvitations( + email: string, + status: string, + pageNumber: number, + pageSize: number, + search = '' + ): Promise { + + this.logger.log(search); + const query = { + email, + status + }; + return this.getOrgInvitationsPagination(query, pageNumber, pageSize); + } + + async getOrgInvitations( + queryObject: object + ): Promise { + try { + return this.prisma.org_invitations.findMany({ + where: { + ...queryObject + }, + include: { + organisation: true + } + }); + } catch (error) { + this.logger.error(`error: ${JSON.stringify(error)}`); + throw new InternalServerErrorException(error); + } + } + + async getOrgInvitationsPagination(queryObject: object, pageNumber: number, pageSize: number): Promise { + try { + const result = await this.prisma.$transaction([ + this.prisma.org_invitations.findMany({ + where: { + ...queryObject + }, + include: { + organisation: true + }, + take: pageSize, + skip: (pageNumber - 1) * pageSize, + orderBy: { + createDateTime: 'desc' + } + }), + this.prisma.org_invitations.count({ + where: { + ...queryObject + } + }) + ]); + + const invitations = result[0]; + const totalCount = result[1]; + const totalPages = Math.ceil(totalCount / pageSize); + + return { totalPages, invitations }; + } catch (error) { + this.logger.error(`error: ${JSON.stringify(error)}`); + throw new InternalServerErrorException(error); + } + } + + async getInvitationsByOrgId(orgId: number, pageNumber: number, pageSize: number, search = ''): Promise { + try { + const query = { + orgId, + OR: [ + { email: { contains: search, mode: 'insensitive' } }, + { status: { contains: search, mode: 'insensitive' } } + ] + }; + + return this.getOrgInvitationsPagination(query, pageNumber, pageSize); + } catch (error) { + this.logger.error(`error: ${JSON.stringify(error)}`); + throw new InternalServerErrorException(error); + } + } + + async getOrganization(orgId: number): Promise { + try { + return this.prisma.organisation.findUnique({ + where: { + id: orgId + }, + include: { + org_agents: { + include: { + agents_type: true, + agent_invitations: true, + org_agent_type: true + } + } + } + }); + } catch (error) { + this.logger.error(`error: ${JSON.stringify(error)}`); + throw new InternalServerErrorException(error); + } + } + + async getOrgDashboard(orgId: number): Promise { + + const query = { + where: { + orgId + } + }; + + try { + + const usersCount = await this.prisma.user_org_roles.count({ + ...query + }); + + const schemasCount = await this.prisma.schema.count({ + ...query + }); + + const credentialsCount = await this.prisma.credentials.count({ + ...query + }); + + const presentationsCount = await this.prisma.presentations.count({ + ...query + }); + + return { + usersCount, + schemasCount, + credentialsCount, + presentationsCount + }; + + } catch (error) { + this.logger.error(`error: ${JSON.stringify(error)}`); + throw new InternalServerErrorException(error); + } + } + + + /** + * + * @param id + * @returns Invitation details + */ + async getInvitationById(id: number): Promise { + try { + return this.prisma.org_invitations.findUnique({ + where: { + id + }, + include: { + organisation: true + } + }); + } catch (error) { + this.logger.error(`error: ${JSON.stringify(error)}`); + throw new InternalServerErrorException(error); + } + } + + /** + * + * @param queryObject + * @param data + * @returns Updated org invitation response + */ + async updateOrgInvitation(id: number, data: object): Promise { + try { + return this.prisma.org_invitations.update({ + where: { + id + }, + data: { + ...data + } + }); + } catch (error) { + this.logger.error(`error: ${JSON.stringify(error)}`); + throw new InternalServerErrorException('Unable to update org invitation'); + } + } + + async getOrganizations( + queryObject: object, + filterOptions: object, + pageNumber: number, + pageSize: number + ): Promise { + try { + const result = await this.prisma.$transaction([ + this.prisma.organisation.findMany({ + where: { + ...queryObject + }, + include: { + userOrgRoles: { + include: { + orgRole: true + }, + where: { + ...filterOptions + // Additional filtering conditions if needed + } + } + }, + take: pageSize, + skip: (pageNumber - 1) * pageSize, + orderBy: { + createDateTime: 'desc' + } + }), + this.prisma.organisation.count({ + where: { + ...queryObject + } + }) + ]); + + const organizations = result[0]; + const totalCount = result[1]; + const totalPages = Math.ceil(totalCount / pageSize); + + return { totalPages, organizations }; + } catch (error) { + this.logger.error(`error: ${JSON.stringify(error)}`); + throw new InternalServerErrorException(error); + } + } +} diff --git a/apps/organization/src/main.ts b/apps/organization/src/main.ts new file mode 100644 index 000000000..a99f36f39 --- /dev/null +++ b/apps/organization/src/main.ts @@ -0,0 +1,22 @@ +import { HttpExceptionFilter } from 'libs/http-exception.filter'; +import { Logger } from '@nestjs/common'; +import { NestFactory } from '@nestjs/core'; +import { OrganizationModule } from './organization.module'; +import { MicroserviceOptions, Transport } from '@nestjs/microservices'; + +const logger = new Logger(); + +async function bootstrap(): Promise { + + const app = await NestFactory.createMicroservice(OrganizationModule, { + transport: Transport.NATS, + options: { + servers: [`${process.env.NATS_URL}`] + } + }); + app.useGlobalFilters(new HttpExceptionFilter()); + + await app.listen(); + logger.log('Organization Microservice is listening to NATS '); +} +bootstrap(); diff --git a/apps/organization/src/organization.controller.ts b/apps/organization/src/organization.controller.ts new file mode 100644 index 000000000..5395c1fff --- /dev/null +++ b/apps/organization/src/organization.controller.ts @@ -0,0 +1,126 @@ +import { Controller, Logger } from '@nestjs/common'; + +import { MessagePattern } from '@nestjs/microservices'; +import { OrganizationService } from './organization.service'; +import { Body } from '@nestjs/common'; +import { CreateOrganizationDto } from '../dtos/create-organization.dto'; +import { BulkSendInvitationDto } from '../dtos/send-invitation.dto'; +import { UpdateInvitationDto } from '../dtos/update-invitation.dt'; +import { IUpdateOrganization } from '../interfaces/organization.interface'; + +@Controller() +export class OrganizationController { + constructor(private readonly organizationService: OrganizationService) { } + private readonly logger = new Logger('OrganizationController'); + + /** + * Description: create new organization + * @param payload Registration Details + * @returns Get created organization details + */ + + @MessagePattern({ cmd: 'create-organization' }) + async createOrganization(@Body() payload: { createOrgDto: CreateOrganizationDto; userId: number }): Promise { + return this.organizationService.createOrganization(payload.createOrgDto, payload.userId); + } + + /** + * Description: update organization + * @param payload Registration Details + * @returns Get updated organization details + */ + + @MessagePattern({ cmd: 'update-organization' }) + async updateOrganization(payload: {updateOrgDto: IUpdateOrganization }): Promise { + return this.organizationService.updateOrganization(payload.updateOrgDto); + } + + + /** + * Description: get organizations + * @param + * @returns Get created organization details + */ + @MessagePattern({ cmd: 'get-organizations' }) + async getOrganizations(@Body() payload: { userId: number, pageNumber: number, pageSize: number, search: string }): Promise { + const { userId, pageNumber, pageSize, search } = payload; + return this.organizationService.getOrganizations(userId, pageNumber, pageSize, search); + } + + /** + * Description: get organization + * @param payload Registration Details + * @returns Get created organization details + */ + @MessagePattern({ cmd: 'get-organization-by-id' }) + async getOrganization(@Body() payload: { orgId: number; userId: number }): Promise { + return this.organizationService.getOrganization(payload.orgId); + } + + /** + * Description: get invitations + * @param orgId + * @returns Get created invitation details + */ + @MessagePattern({ cmd: 'get-invitations-by-orgId' }) + async getInvitationsByOrgId(@Body() payload: { orgId: number, pageNumber: number, pageSize: number, search: string }): Promise { + return this.organizationService.getInvitationsByOrgId(payload.orgId, payload.pageNumber, payload.pageSize, payload.search); + } + + /** + * Description: retrieve org-roles + * @returns Get org-roles details + */ + + @MessagePattern({ cmd: 'get-org-roles' }) + async getOrgRoles(): Promise { + return this.organizationService.getOrgRoles(); + } + + /** + * Description: create new organization invitation + * @param payload invitation Details + * @returns Get created organization invitation details + */ + @MessagePattern({ cmd: 'send-invitation' }) + async createInvitation( + @Body() payload: { bulkInvitationDto: BulkSendInvitationDto; userId: number } + ): Promise { + return this.organizationService.createInvitation(payload.bulkInvitationDto, payload.userId); + } + + @MessagePattern({ cmd: 'fetch-user-invitations' }) + async fetchUserInvitation(@Body() payload: { + email: string; status: string, pageNumber: number, pageSize: number, search: string + }): Promise { + return this.organizationService.fetchUserInvitation(payload.email, payload.status, payload.pageNumber, payload.pageSize, payload.search); + } + + /** + * + * @param payload + * @returns Updated invitation status + */ + @MessagePattern({ cmd: 'update-invitation-status' }) + async updateOrgInvitation(@Body() payload: UpdateInvitationDto): Promise { + return this.organizationService.updateOrgInvitation(payload); + } + + /** + * + * @param payload + * @returns Update user roles response + */ + + @MessagePattern({ cmd: 'update-user-roles' }) + async updateUserRoles(payload: { orgId: number; roleIds: number[]; userId: number }): Promise { + return this.organizationService.updateUserRoles(payload.orgId, payload.roleIds, payload.userId); + } + + @MessagePattern({ cmd: 'get-organization-dashboard' }) + async getOrgDashboard(payload: { orgId: number; userId: number }): Promise { + return this.organizationService.getOrgDashboard(payload.orgId); + } + + +} diff --git a/apps/organization/src/organization.module.ts b/apps/organization/src/organization.module.ts new file mode 100644 index 000000000..6ba7d20e3 --- /dev/null +++ b/apps/organization/src/organization.module.ts @@ -0,0 +1,31 @@ +import { CommonModule } from '@credebl/common'; +import { Module, Logger } from '@nestjs/common'; +import { ClientsModule, Transport } from '@nestjs/microservices'; +import { OrganizationController } from './organization.controller'; +import { OrganizationRepository } from '../repositories/organization.repository'; +import { OrganizationService } from './organization.service'; +import { PrismaService } from '@credebl/prisma-service'; +import { OrgRolesService } from '@credebl/org-roles'; +import { UserOrgRolesService } from '@credebl/user-org-roles'; +import { OrgRolesRepository } from 'libs/org-roles/repositories'; +import { UserOrgRolesRepository } from 'libs/user-org-roles/repositories'; +import { UserRepository } from 'apps/user/repositories/user.repository'; + +@Module({ + imports: [ + ClientsModule.register([ + { + name: 'NATS_CLIENT', + transport: Transport.NATS, + options: { + servers: [`${process.env.NATS_URL}`] + } + } + ]), + CommonModule + ], + controllers: [OrganizationController], + providers: [OrganizationService, OrganizationRepository, PrismaService, Logger, OrgRolesService, UserOrgRolesService, OrgRolesRepository, UserOrgRolesRepository, UserRepository] + +}) +export class OrganizationModule {} diff --git a/apps/organization/src/organization.service.ts b/apps/organization/src/organization.service.ts new file mode 100644 index 000000000..8f59a64eb --- /dev/null +++ b/apps/organization/src/organization.service.ts @@ -0,0 +1,379 @@ +// eslint-disable-next-line camelcase +import { organisation, org_roles, user } from '@prisma/client'; +import { Injectable, Logger, ConflictException, InternalServerErrorException, HttpException } from '@nestjs/common'; +import { PrismaService } from '@credebl/prisma-service'; +import { CommonService } from '@credebl/common'; +import { OrganizationRepository } from '../repositories/organization.repository'; +import { ClientProxy, RpcException } from '@nestjs/microservices'; +import { Inject } from '@nestjs/common'; +import { OrgRolesService } from '@credebl/org-roles'; +import { OrgRoles } from 'libs/org-roles/enums'; +import { UserOrgRolesService } from '@credebl/user-org-roles'; +import { ResponseMessages } from '@credebl/common/response-messages'; +import { OrganizationInviteTemplate } from '../templates/organization-invitation.template'; +import { EmailDto } from '@credebl/common/dtos/email.dto'; +import { sendEmail } from '@credebl/common/send-grid-helper-file'; +import { CreateOrganizationDto } from '../dtos/create-organization.dto'; +import { BulkSendInvitationDto } from '../dtos/send-invitation.dto'; +import { UpdateInvitationDto } from '../dtos/update-invitation.dt'; +import { NotFoundException } from '@nestjs/common'; +import { Invitation } from '@credebl/enum/enum'; +import { IUpdateOrganization } from '../interfaces/organization.interface'; +@Injectable() +export class OrganizationService { + constructor( + private readonly prisma: PrismaService, + private readonly commonService: CommonService, + @Inject('NATS_CLIENT') private readonly organizationServiceProxy: ClientProxy, + private readonly organizationRepository: OrganizationRepository, + private readonly orgRoleService: OrgRolesService, + private readonly userOrgRoleService: UserOrgRolesService, + private readonly logger: Logger + ) { } + + /** + * + * @param registerOrgDto + * @returns + */ + + // eslint-disable-next-line camelcase + async createOrganization(createOrgDto: CreateOrganizationDto, userId: number): Promise { + try { + const organizationExist = await this.organizationRepository.checkOrganizationNameExist(createOrgDto.name); + + if (organizationExist) { + throw new ConflictException(ResponseMessages.organisation.error.exists); + } + + const organizationDetails = await this.organizationRepository.createOrganization(createOrgDto); + + const ownerRoleData = await this.orgRoleService.getRole(OrgRoles.OWNER); + + await this.userOrgRoleService.createUserOrgRole(userId, ownerRoleData.id, organizationDetails.id); + return organizationDetails; + } catch (error) { + this.logger.error(`In create organization : ${JSON.stringify(error)}`); + throw new RpcException(error.response); + } + } + + /** + * + * @param registerOrgDto + * @returns + */ + + // eslint-disable-next-line camelcase + async updateOrganization(updateOrgDto: IUpdateOrganization): Promise { + try { + const organizationDetails = await this.organizationRepository.updateOrganization(updateOrgDto); + return organizationDetails; + } catch (error) { + this.logger.error(`In update organization : ${JSON.stringify(error)}`); + throw new RpcException(error.response); + } + } + + /** + * Description: get organizations + * @param + * @returns Get created organizations details + */ + // eslint-disable-next-line camelcase + async getOrganizations(userId: number, pageNumber: number, pageSize: number, search: string): Promise { + try { + + const query = { + userOrgRoles: { + some: { userId } + }, + OR: [ + { name: { contains: search } }, + { description: { contains: search } } + ] + }; + + const filterOptions = { + userId + }; + + return this.organizationRepository.getOrganizations( + query, + filterOptions, + pageNumber, + pageSize + ); + + } catch (error) { + this.logger.error(`In fetch getOrganizations : ${JSON.stringify(error)}`); + throw new RpcException(error.response); + } + } + + + /** + * Description: get organization + * @param orgId Registration Details + * @returns Get created organization details + */ + // eslint-disable-next-line camelcase + async getOrganization(orgId: number): Promise { + try { + const getOrganization = await this.organizationRepository.getOrganization(orgId); + return getOrganization; + } catch (error) { + this.logger.error(`In create organization : ${JSON.stringify(error)}`); + throw new RpcException(error.response); + } + } + + /** + * Description: get invitation + * @param orgId Registration Details + * @returns Get created invitation details + */ + // eslint-disable-next-line camelcase + async getInvitationsByOrgId(orgId: number, pageNumber: number, pageSize: number, search: string): Promise { + try { + 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)}`); + throw new RpcException(error.response); + } + } + + /** + * + * @param registerOrgDto + * @returns + */ + + // eslint-disable-next-line camelcase + async getOrgRoles(): Promise { + try { + return this.orgRoleService.getOrgRoles(); + } catch (error) { + this.logger.error(`In getOrgRoles : ${JSON.stringify(error)}`); + throw new RpcException(error.response); + } + } + + /** + * + * @param email + * @returns + */ + async checkInvitationExist( + email: string, + orgId: number + ): Promise { + try { + + const query = { + email, + orgId + }; + + const invitations = await this.organizationRepository.getOrgInvitations(query); + + if (0 < invitations.length) { + return true; + } + return false; + } catch (error) { + this.logger.error(`error: ${JSON.stringify(error)}`); + throw new InternalServerErrorException(error); + } + } + + /** + * + * @Body sendInvitationDto + * @returns createInvitation + */ + + // eslint-disable-next-line camelcase + async createInvitation(bulkInvitationDto: BulkSendInvitationDto, userId: number): Promise { + const { invitations, orgId } = bulkInvitationDto; + + try { + const organizationDetails = await this.organizationRepository.getOrganizationDetails(orgId); + + for (const invitation of invitations) { + const { orgRoleId, email } = invitation; + + const isUserExist = await this.checkUserExistInPlatform(email); + + const isInvitationExist = await this.checkInvitationExist(email, orgId); + + if (!isInvitationExist) { + await this.organizationRepository.createSendInvitation(email, orgId, userId, orgRoleId); + + const orgRolesDetails = await this.orgRoleService.getOrgRolesByIds(orgRoleId); + try { + await this.sendInviteEmailTemplate(email, organizationDetails.name, orgRolesDetails, isUserExist); + } catch (error) { + throw new InternalServerErrorException(ResponseMessages.user.error.emailSend); + } + } + + } + + return ResponseMessages.organisation.success.createInvitation; + } catch (error) { + this.logger.error(`In send Invitation : ${JSON.stringify(error)}`); + throw new RpcException(error.response); + } + } + + /** + * + * @param email + * @param orgName + * @param orgRolesDetails + * @returns true/false + */ + + async sendInviteEmailTemplate( + email: string, + orgName: string, + orgRolesDetails: object[], + isUserExist: boolean + ): Promise { + const platformConfigData = await this.prisma.platform_config.findMany(); + + const urlEmailTemplate = new OrganizationInviteTemplate(); + const emailData = new EmailDto(); + emailData.emailFrom = platformConfigData[0].emailFrom; + emailData.emailTo = email; + emailData.emailSubject = `${process.env.PLATFORM_NAME} Platform: Invitation`; + + emailData.emailHtml = await urlEmailTemplate.sendInviteEmailTemplate(email, orgName, orgRolesDetails, isUserExist); + + //Email is sent to user for the verification through emailData + const isEmailSent = await sendEmail(emailData); + + return isEmailSent; + } + + async checkUserExistInPlatform(email: string): Promise { + const pattern = { cmd: 'get-user-by-mail' }; + const payload = { email }; + + const userData: user = 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 + ); + }); + + if (userData && userData.isEmailVerified) { + return true; + } + return false; + } + + async fetchUserInvitation(email: string, status: string, pageNumber: number, pageSize: number, search = ''): Promise { + try { + return this.organizationRepository.getAllOrgInvitations(email, status, pageNumber, pageSize, search); + } catch (error) { + this.logger.error(`In fetchUserInvitation : ${JSON.stringify(error)}`); + throw new RpcException(error.response); + } + } + + /** + * + * @param payload + * @returns Updated invitation response + */ + async updateOrgInvitation(payload: UpdateInvitationDto): Promise { + try { + const { orgId, status, invitationId, userId } = payload; + + const invitation = await this.organizationRepository.getInvitationById(invitationId); + + if (!invitation) { + throw new NotFoundException(ResponseMessages.user.error.invitationNotFound); + } + + const data = { + status + }; + + await this.organizationRepository.updateOrgInvitation(invitationId, data); + + if (status === Invitation.REJECTED) { + return ResponseMessages.user.success.invitationReject; + } + 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); + } + } + +/** + * + * @param orgId + * @param roleIds + * @param userId + * @returns + */ + async updateUserRoles(orgId: number, roleIds: number[], userId: number): 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); + + 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); + + } catch (error) { + this.logger.error(`Error in updateUserRoles: ${JSON.stringify(error)}`); + throw new RpcException(error.response); + } + } + + async getOrgDashboard(orgId: number): Promise { + try { + return this.organizationRepository.getOrgDashboard(orgId); + } catch (error) { + this.logger.error(`In create organization : ${JSON.stringify(error)}`); + throw new RpcException(error.response); + } + } + +} diff --git a/apps/organization/templates/organization-invitation.template.ts b/apps/organization/templates/organization-invitation.template.ts new file mode 100644 index 000000000..8cc0c807c --- /dev/null +++ b/apps/organization/templates/organization-invitation.template.ts @@ -0,0 +1,73 @@ +export class OrganizationInviteTemplate { + + public sendInviteEmailTemplate( + email: string, + orgName: string, + orgRolesDetails: object[], + isUserExist = false + ): string { + + const validUrl = isUserExist ? `${process.env.FRONT_END_URL}/authentication/sign-in` : `${process.env.FRONT_END_URL}/authentication/sign-up`; + + const message = isUserExist + ? `You have already registered on platform, you can access the application depending on your role right away. + Please log in and accept the oranizations “INVITATION” and join the organization for specific roles` + : `You have to register on the platform and then you can access the application. Accept the oranizations “INVITATION” and join the organization for specific roles`; + + const year: number = new Date().getFullYear(); + + return ` + + + + + + + + + +
+ +
+

+ Hello ${email}, +

+

+ Congratulations! + Your have been successfully invited to join. +

    +
  • Organization: ${orgName}
  • +
  • Organization's Role: ${orgRolesDetails.map(roleObje => roleObje['name'])}
  • +
+ ${message} + + +

In case you need any assistance to access your account, please contact CREDEBL Platform +

+
+
+

+ ® CREDEBL ${year}, Powered by Blockster Labs. All Rights Reserved. +

+
+
+
+ + + `; + + } + + +} \ No newline at end of file diff --git a/apps/organization/templates/organization-onboard.template.ts b/apps/organization/templates/organization-onboard.template.ts new file mode 100644 index 000000000..76362276c --- /dev/null +++ b/apps/organization/templates/organization-onboard.template.ts @@ -0,0 +1,85 @@ +export class OnBoardVerificationRequest { + + public getOnBoardRequest(orgName: string, email: string): string { + const year: number = new Date().getFullYear(); + + try { + return ` + + + WELCOME TO CREDEBL + + + + + +
+
+ Credebl Logo +
+
+ Invite Image +
+
+

Hello ${email},

+

The ${orgName} has been sent an onboard request

+

+ In case you need any assistance to access your account, please contact + Blockster Labs +

+
+
+
+ + f + t +
+

+ ® CREDEBL ${year}, Powered by Blockster Labs. All Rights Reserved. +

+
+
+
+ + `; + } catch (error) { + } + } +} \ No newline at end of file diff --git a/apps/organization/templates/organization-url-template.ts b/apps/organization/templates/organization-url-template.ts new file mode 100644 index 000000000..58d664f90 --- /dev/null +++ b/apps/organization/templates/organization-url-template.ts @@ -0,0 +1,81 @@ +import * as url from 'url'; + +export class URLOrganizationEmailTemplate { + + public getOrganizationURLTemplate(orgName: string, email: string, verificationCode: string, type: string): string { + const endpoint = `${process.env.API_GATEWAY_PROTOCOL}://${process.env.API_ENDPOINT}`; + const year: number = new Date().getFullYear(); + let apiUrl; + + if ('ADMIN' === type) { + apiUrl = url.parse(`${endpoint}/email/tenant/verify?verificationCode=${verificationCode}&email=${encodeURIComponent(email)}`); + } else { + apiUrl = url.parse(`${endpoint}/email/non-admin-user/verify?verificationCode=${verificationCode}&email=${encodeURIComponent(email)}`); + } + + const validUrl = apiUrl.href.replace('/:', ':'); + try { + return ` + + + + + + + + + +
+
+ Credebl Logo +
+
+ verification Image +
+
+

+ Hello ${orgName}, +

+

+ Your organisation ${orgName} has been successfully created on ${process.env.PLATFORM_NAME}. In order to enable access for your account, + we need to verify your email address. Please use the link below or click on the “Verify” button to enable access to your account. +

+

Your account details as follows,

+
    +
  • Username/Email: ${email}
  • +
  • Verification Link: ${validUrl}
  • +
+ +

In case you need any assistance to access your account, please contact Blockster Labs +

+
+
+
+ + f + t +
+

+ ® CREDEBL ${year}, Powered by Blockster Labs. All Rights Reserved. +

+
+
+
+ + `; + + } catch (error) { + } + } +} + diff --git a/apps/organization/tsconfig.app.json b/apps/organization/tsconfig.app.json new file mode 100644 index 000000000..36b277d10 --- /dev/null +++ b/apps/organization/tsconfig.app.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "declaration": false, + "outDir": "../../dist/apps/organization" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] +} diff --git a/apps/user/Dockerfile b/apps/user/Dockerfile new file mode 100644 index 000000000..f1b337535 --- /dev/null +++ b/apps/user/Dockerfile @@ -0,0 +1,43 @@ +# Stage 1: Build the application +FROM node:16-alpine as build + +# Set the working directory +WORKDIR /app + +# Copy package.json and package-lock.json +COPY package.json ./ +#COPY package-lock.json ./ + +# Install dependencies +RUN npm i + +# Copy the rest of the application code +COPY . . +CMD ["sh", "-c", "cd libs/prisma-service && npx prisma generate"] + +# Build the user service +RUN npm run build user + +# Stage 2: Create the final image +FROM node:16-alpine + +# Set the working directory +WORKDIR /app + +# Copy the compiled code from the build stage +COPY --from=build /app/dist/apps/user/ ./dist/apps/user/ + +# 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 + +# Install production dependencies for the final image +#RUN npm i --only=production + +# Set the command to run the microservice +CMD ["sh", "-c", "cd libs/prisma-service && npx prisma generate && cd ../.. && node dist/apps/user/main.js"] + +# docker build -t user -f apps/user/Dockerfile . +# docker run -d --env-file .env --name user docker.io/library/user +# docker logs -f user diff --git a/apps/user/dtos/accept-reject-invitation.dto.ts b/apps/user/dtos/accept-reject-invitation.dto.ts new file mode 100644 index 000000000..0885db95b --- /dev/null +++ b/apps/user/dtos/accept-reject-invitation.dto.ts @@ -0,0 +1,7 @@ +import { Invitation } from '@credebl/enum/enum'; + +export class AcceptRejectInvitationDto { + invitationId: number; + orgId: number; + status: Invitation; +} diff --git a/apps/user/dtos/create-user.dto.ts b/apps/user/dtos/create-user.dto.ts new file mode 100644 index 000000000..a0372f080 --- /dev/null +++ b/apps/user/dtos/create-user.dto.ts @@ -0,0 +1,6 @@ +export class CreateUserDto { + email: string; + password: string; + firstName: string; + lastName: string; +} \ No newline at end of file diff --git a/apps/user/dtos/keycloak-register.dto.ts b/apps/user/dtos/keycloak-register.dto.ts new file mode 100644 index 000000000..bfeb2c715 --- /dev/null +++ b/apps/user/dtos/keycloak-register.dto.ts @@ -0,0 +1,29 @@ +export class KeycloakUserRegistrationDto { + email: string; + firstName: string; + lastName: string; + username: string; + enabled: boolean; + totp: boolean; + emailVerified: boolean; + notBefore: number; + credentials: Credentials[]; + access: Access; + realmRoles: string[]; + attributes: object; + +} + +export class Credentials { + type: string; + value: string; + temporary: boolean; +} + +export class Access { + manageGroupMembership: boolean; + view: boolean; + mapRoles: boolean; + impersonate: boolean; + manage: boolean; +} \ No newline at end of file diff --git a/apps/user/dtos/login-user.dto.ts b/apps/user/dtos/login-user.dto.ts new file mode 100644 index 000000000..d0d64935b --- /dev/null +++ b/apps/user/dtos/login-user.dto.ts @@ -0,0 +1,21 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsBoolean, IsEmail, IsNotEmpty, IsOptional, IsString } from 'class-validator'; + + +export class LoginUserDto { + @ApiProperty({ example: 'awqx@getnada.com' }) + @IsEmail() + @IsNotEmpty({ message: 'Please provide valid email' }) + @IsString({ message: 'email should be string' }) + email: string; + + @ApiProperty({ example: 'Password@1' }) + @IsOptional() + @IsString({ message: 'password should be string' }) + password: string; + + @ApiProperty({ example: 'false' }) + @IsOptional() + @IsBoolean({ message: 'isPasskey should be boolean' }) + isPasskey: boolean; +} diff --git a/apps/user/dtos/verify-email.dto.ts b/apps/user/dtos/verify-email.dto.ts new file mode 100644 index 000000000..0355265de --- /dev/null +++ b/apps/user/dtos/verify-email.dto.ts @@ -0,0 +1,4 @@ +export class VerifyEmailTokenDto { + email: string; + verificationCode: string; +} diff --git a/apps/user/interfaces/user.interface.ts b/apps/user/interfaces/user.interface.ts new file mode 100644 index 000000000..41abb9ae3 --- /dev/null +++ b/apps/user/interfaces/user.interface.ts @@ -0,0 +1,44 @@ + + +export interface UserI { + id?: number, + username?: string, + email?: string, + firstName?: string, + lastName?: string, + isEmailVerified?: boolean, + clientId?: string, + clientSecret?: string, + keycloakUserId?: string, + userOrgRoles?: object +} + +export interface InvitationsI { + id: number, + userId: number, + orgId?: number, + organisation?: object + orgRoleId?: number, + status: string, + email?: string + orgRoles: number[] +} + +export interface UserEmailVerificationDto{ + email:string +} + +export interface userInfo{ + password: string, + firstName: string, + lastName: string, + isPasskey: boolean +} + +export interface UserWhereUniqueInput{ + id?: number +} + +export interface UserWhereInput{ + email?: string +} \ No newline at end of file diff --git a/apps/user/repositories/fido-user.repository.ts b/apps/user/repositories/fido-user.repository.ts new file mode 100644 index 000000000..8c82d8b6a --- /dev/null +++ b/apps/user/repositories/fido-user.repository.ts @@ -0,0 +1,132 @@ +import * as bcrypt from 'bcrypt'; + +import { Injectable, Logger, NotFoundException } from '@nestjs/common'; +import { CreateUserDto } from '../dtos/create-user.dto'; +import { InternalServerErrorException } from '@nestjs/common'; +import { PrismaService } from '@credebl/prisma-service'; +import { user } from '@prisma/client'; +import { v4 as uuidv4 } from 'uuid'; + +type UserUpdateData = { + fidoUserId?: string; + isFidoVerified?: boolean; + username?: string; + // Add other properties you want to update +}; + +@Injectable() +export class FidoUserRepository { + constructor(private readonly prisma: PrismaService, private readonly logger: Logger) { } + + + /** + * + * @param createUserDto + * @returns user details + */ + async createUser(createUserDto: CreateUserDto): Promise { + try { + const verifyCode = uuidv4(); + + const saveResponse = await this.prisma.user.create({ + data: { + username: createUserDto.email, + email: createUserDto.email, + firstName: createUserDto.firstName, + lastName: createUserDto.lastName, + password: await bcrypt.hash(createUserDto.password, 10), + verificationCode: verifyCode + } + }); + + return saveResponse; + + } catch (error) { + this.logger.error(`In Create User Repository: ${JSON.stringify(error)}`); + throw error; + } + } + + /** + * + * @param email + * @returns User exist details + */ + + // eslint-disable-next-line camelcase + async checkFidoUserExist(email: string): Promise { + try { + return this.prisma.user.findFirst({ + where: { + email + } + }); + } catch (error) { + this.logger.error(`checkUserExist: ${JSON.stringify(error)}`); + throw new InternalServerErrorException(error); + } + } + + /** + * + * @param email + * @returns User details + */ + + // eslint-disable-next-line camelcase + async getUserDetails(email: string): Promise { + try { + return this.prisma.user.findFirst({ + where: { + email + } + }); + } catch (error) { + this.logger.error(`Not Found: ${JSON.stringify(error)}`); + throw new NotFoundException(error); + } + } + + /** + * + * @param tenantDetails + * @returns Updates organization details + */ + // eslint-disable-next-line camelcase + async updateFidoUserDetails(email:string, fidoUserId: string, username: string): Promise { + try { + const updateUserDetails = await this.prisma.user.update({ + where: { + email + }, + data: { + fidoUserId, + username + } + }); + return updateUserDetails; + + } catch (error) { + this.logger.error(`Error in update isEmailVerified: ${error.message} `); + throw error; + } + } + + + async updateUserDetails(email:string, additionalParams:UserUpdateData[]): Promise { + try { + const updateUserDetails = await this.prisma.user.update({ + where: { + email + }, + data: { ...additionalParams[0]} + }); + return updateUserDetails; + + } catch (error) { + this.logger.error(`Error in update isEmailVerified: ${error.message} `); + throw error; + } + } +} + diff --git a/apps/user/repositories/user-device.repository.ts b/apps/user/repositories/user-device.repository.ts new file mode 100644 index 000000000..5e4422905 --- /dev/null +++ b/apps/user/repositories/user-device.repository.ts @@ -0,0 +1,290 @@ +import { Injectable, Logger, NotFoundException } from '@nestjs/common'; +import { InternalServerErrorException } from '@nestjs/common'; +import { PrismaService } from '@credebl/prisma-service'; +// eslint-disable-next-line camelcase +import { Prisma, user_devices } from '@prisma/client'; + +type FidoMultiDevicePayload = { + createDateTime: Date; + createdBy: number; + lastChangedDateTime: Date; + lastChangedBy: number; + devices: Prisma.JsonValue; + credentialId: string; + deviceFriendlyName: string; + id: number; +}[]; +@Injectable() +export class UserDevicesRepository { + constructor(private readonly prisma: PrismaService, private readonly logger: Logger) { } + + /** + * + * @param email + * @returns User exist details + */ + + // eslint-disable-next-line camelcase + async checkUserDevice(userId: number): Promise { + try { + return this.prisma.user_devices.findFirst({ + where: { + userId + } + }); + } catch (error) { + this.logger.error(`checkUserExist: ${JSON.stringify(error)}`); + throw new InternalServerErrorException(error); + } + } + +/** + * + * @param createFidoMultiDevice + * @returns Device details + */ +// eslint-disable-next-line camelcase +async createMultiDevice(newDevice:Prisma.JsonValue, userId:number): Promise { + try { + + const saveResponse = await this.prisma.user_devices.create({ + data: { + devices: newDevice, + userId + } + }); + + return saveResponse; + + } catch (error) { + this.logger.error(`In Create User Repository: ${JSON.stringify(error)}`); + throw error; + } + } + +/** + * + * @param userId + * @returns Device details + */ + // eslint-disable-next-line camelcase + async fidoMultiDevice(userId: number): Promise { + try { + const userDetails = await this.prisma.user_devices.findMany({ + where: { + userId, + deletedAt: null + }, + orderBy: { + createDateTime: 'desc' + } + }); + + return userDetails; + } catch (error) { + this.logger.error(`Not Found: ${JSON.stringify(error)}`); + throw new NotFoundException(error); + } +} + +/** + * + * @param userId + * @returns Get all device details + */ +// eslint-disable-next-line camelcase, @typescript-eslint/no-explicit-any +async getfidoMultiDevice(userId: number): Promise { + try { + + const fidoMultiDevice = await this.prisma.user_devices.findMany({ + where: { + userId + } + }); + return fidoMultiDevice; + } catch (error) { + this.logger.error(`Not Found: ${JSON.stringify(error)}`); + throw new NotFoundException(error); + } +} + +/** + * + * @param userId + * @returns Get all active device details + */ +async getfidoMultiDeviceDetails(userId: number): Promise { + try { + const fidoMultiDevice = await this.prisma.user_devices.findMany({ + where: { + userId, + deletedAt: null + }, + select: { + id: true, + createDateTime: true, + createdBy: true, + lastChangedDateTime: true, + lastChangedBy: true, + devices: true, + credentialId: true, + deviceFriendlyName: true + } + }); + return fidoMultiDevice; + } catch (error) { + this.logger.error(`Not Found: ${JSON.stringify(error)}`); + throw new NotFoundException(error); + } +} + +/** + * + * @param credentialId + * @returns Find device details from credentialID + */ +async getFidoUserDeviceDetails(credentialId: string): Promise { + this.logger.log(`credentialId: ${credentialId}`); + try { + const getUserDevice = await this.prisma.$queryRaw` + SELECT * FROM user_devices + WHERE credentialId LIKE '%${credentialId}%' + LIMIT 1; +`; + return getUserDevice; + } catch (error) { + this.logger.error(`Not Found: ${JSON.stringify(error)}`); + throw new NotFoundException(error); + } +} + +/** + * + * @param credentialId + * @param loginCounter + * @returns Update Auth counter + */ +async updateFidoAuthCounter(credentialId: string, loginCounter:number): Promise { + try { + return await this.prisma.user_devices.updateMany({ + where: { + credentialId + }, + data: { + authCounter: loginCounter + } + }); + + } catch (error) { + this.logger.error(`Not Found: ${JSON.stringify(error)}`); + throw new NotFoundException(error); + } +} + +/** + * + * @param credentialId + * @returns Device detail for specific credentialId + */ +// eslint-disable-next-line camelcase +async checkUserDeviceByCredentialId(credentialId: string): Promise { + this.logger.log(`checkUserDeviceByCredentialId: ${credentialId}`); + try { + return this.prisma.user_devices.findFirst({ + where: { + credentialId + } + }); + } catch (error) { + this.logger.error(`checkUserExist: ${JSON.stringify(error)}`); + throw new InternalServerErrorException(error); + } +} + +/** + * + * @param credentialId + * @returns Delete device + */ +// eslint-disable-next-line camelcase +async deleteUserDeviceByCredentialId(credentialId: string): Promise { + try { + return await this.prisma.user_devices.updateMany({ + where: { + credentialId + }, + data: { + deletedAt: new Date() + } + }); + } catch (error) { + this.logger.error(`checkUserExist: ${JSON.stringify(error)}`); + throw new InternalServerErrorException(error); + } +} +/** + * + * @param id + * @param deviceName + * @returns Update device name + */ +async updateUserDeviceByCredentialId(id: number, deviceName:string): Promise { + try { + return await this.prisma.user_devices.updateMany({ + where: { + id + }, + data: { + deviceFriendlyName: deviceName + } + }); + } catch (error) { + this.logger.error(`checkUserExist: ${JSON.stringify(error)}`); + throw new InternalServerErrorException(error); + } +} + +/** + * + * @param credentialId + * @param deviceFriendlyName + * @returns Get device details name for specific credentialId + */ +async updateDeviceByCredentialId(credentialId:string): Promise { + try { + return await this.prisma.$queryRaw` + SELECT * FROM user_devices + WHERE devices->>'credentialID' = ${credentialId} + `; + } catch (error) { + this.logger.error(`checkUserExist: ${JSON.stringify(error)}`); + throw new InternalServerErrorException(error); + } +} + +/** + * + * @param id + * @param credentialId + * @param deviceFriendlyName + * @returns Update device name for specific credentialId + */ +// eslint-disable-next-line camelcase +async addCredentialIdAndNameById(id:number, credentialId:string, deviceFriendlyName:string): Promise { + try { + return await this.prisma.user_devices.update({ + where: { + id + }, + data: { + credentialId, + deviceFriendlyName + } + }); + } catch (error) { + this.logger.error(`checkUserExist: ${JSON.stringify(error)}`); + throw new InternalServerErrorException(error); + } +} +} + diff --git a/apps/user/repositories/user.repository.ts b/apps/user/repositories/user.repository.ts new file mode 100644 index 000000000..01ea60094 --- /dev/null +++ b/apps/user/repositories/user.repository.ts @@ -0,0 +1,334 @@ +/* eslint-disable prefer-destructuring */ + +import * as bcrypt from 'bcrypt'; +import { Injectable, Logger, NotFoundException } from '@nestjs/common'; +import { InternalServerErrorException } from '@nestjs/common'; +import { PrismaService } from '@credebl/prisma-service'; +import { UserEmailVerificationDto, UserI, userInfo } from '../interfaces/user.interface'; +// eslint-disable-next-line camelcase +import { user } from '@prisma/client'; +import { v4 as uuidv4 } from 'uuid'; + +interface UserQueryOptions { + id?: number; // Use the appropriate type based on your data model + email?: string; // Use the appropriate type based on your data model + // Add more properties if needed for other unique identifier fields +}; + +@Injectable() +export class UserRepository { + constructor(private readonly prisma: PrismaService, private readonly logger: Logger) { } + + /** + * + * @param userEmailVerificationDto + * @returns user email + */ + async createUser(userEmailVerificationDto: UserEmailVerificationDto): Promise { + try { + const verifyCode = uuidv4(); + const saveResponse = await this.prisma.user.create({ + data: { + username: userEmailVerificationDto.email, + email: userEmailVerificationDto.email, + verificationCode: verifyCode.toString() + } + }); + + return saveResponse; + } catch (error) { + this.logger.error(`In Create User Repository: ${JSON.stringify(error)}`); + throw error; + } + } + + /** + * + * @param email + * @returns User exist details + */ + + // eslint-disable-next-line camelcase + async checkUserExist(email: string): Promise { + try { + return this.prisma.user.findFirst({ + where: { + email + } + }); + } catch (error) { + this.logger.error(`checkUserExist: ${JSON.stringify(error)}`); + throw new InternalServerErrorException(error); + } + } + + /** + * + * @param email + * @returns User details + */ + async getUserDetails(email: string): Promise { + try { + return this.prisma.user.findFirst({ + where: { + email + } + }); + } catch (error) { + this.logger.error(`Not Found: ${JSON.stringify(error)}`); + throw new NotFoundException(error); + } + + } + + /** + * + * @param id + * @returns User profile data + */ + async getUserById(id: number): Promise { + + const queryOptions: UserQueryOptions = { + id + }; + + return this.findUser(queryOptions); + } + + /** + * + * @param id + * @returns User data + */ + async getUserByKeycloakId(id: string): Promise { + try { + return this.prisma.user.findFirst({ + where: { + keycloakUserId: id + }, + select: { + id: true, + username: true, + password: false, + email: true, + firstName: true, + lastName: true, + isEmailVerified: true, + clientId: true, + clientSecret: true, + keycloakUserId: true, + userOrgRoles: { + include: { + orgRole: true, + organisation: { + include: { + // eslint-disable-next-line camelcase + org_agents: true + } + } + } + } + } + }); + } catch (error) { + this.logger.error(`Not Found: ${JSON.stringify(error)}`); + throw new NotFoundException(error); + } + + } + + async findUserByEmail(email: string): Promise { + const queryOptions: UserQueryOptions = { + email + }; + return this.findUser(queryOptions); + } + + async findUser(queryOptions: UserQueryOptions): Promise { + return this.prisma.user.findFirst({ + where: { + OR: [ + { + id: queryOptions.id + }, + { + email: queryOptions.email + } + ] + }, + select: { + id: true, + username: true, + password: false, + email: true, + firstName: true, + lastName: true, + isEmailVerified: true, + clientId: true, + clientSecret: true, + keycloakUserId: true, + userOrgRoles: { + include: { + orgRole: true, + organisation: { + include: { + // eslint-disable-next-line camelcase + org_agents: { + include: { + // eslint-disable-next-line camelcase + agents_type: true + } + } + } + } + } + } + } + }); + } + + /** + * + * @param tenantDetails + * @returns Updates organization details + */ + // eslint-disable-next-line camelcase + async updateUserDetails(id: number, keycloakUserId: string): Promise { + try { + const updateUserDetails = await this.prisma.user.update({ + where: { + id + }, + data: { + isEmailVerified: true, + keycloakUserId + } + }); + return updateUserDetails; + } catch (error) { + this.logger.error(`Error in update isEmailVerified: ${error.message} `); + throw error; + } + } + + /** + * + * @param userInfo + * @returns Updates user details + */ + // eslint-disable-next-line camelcase + async updateUserInfo(email: string, userInfo: userInfo): Promise { + try { + const updateUserDetails = await this.prisma.user.update({ + where: { + email + }, + data: { + firstName: userInfo.firstName, + lastName: userInfo.lastName, + password: await bcrypt.hash(userInfo.password, 10) + } + }); + return updateUserDetails; + } catch (error) { + this.logger.error(`Error in update isEmailVerified: ${error.message} `); + throw error; + } + } + + /** + * + * @param queryOptions + * @param filterOptions + * @returns users list + */ + async findUsers(queryOptions: object, pageNumber: number, pageSize: number, filterOptions?: object): Promise { + + const result = await this.prisma.$transaction([ + this.prisma.user.findMany({ + where: { + ...queryOptions // Spread the dynamic condition object + }, + select: { + id: true, + username: true, + password: false, + email: true, + firstName: true, + lastName: true, + isEmailVerified: true, + clientId: true, + clientSecret: true, + keycloakUserId: true, + userOrgRoles: { + where: { + ...filterOptions + // Additional filtering conditions if needed + }, + include: { + orgRole: true, + organisation: { + include: { + // eslint-disable-next-line camelcase + org_agents: { + include: { + // eslint-disable-next-line camelcase + agents_type: true + } + } + } + } + } + } + }, + take: pageSize, + skip: (pageNumber - 1) * pageSize, + orderBy: { + createDateTime: 'desc' + } + }), + this.prisma.user.count({ + where: { + ...queryOptions + } + }) + ]); + + const users = result[0]; + const totalCount = result[1]; + const totalPages = Math.ceil(totalCount / pageSize); + + return { totalPages, users }; + } + + async checkUniqueUserExist(email: string): Promise { + try { + return this.prisma.user.findUnique({ + where: { + email + } + }); + } catch (error) { + this.logger.error(`checkUserExist: ${JSON.stringify(error)}`); + throw new InternalServerErrorException(error); + } + } + + async verifyUser(email: string): Promise { + try { + const updateUserDetails = await this.prisma.user.update({ + where: { + email + }, + data: { + isEmailVerified: true + } + }); + return updateUserDetails; + } catch (error) { + this.logger.error(`Error in update isEmailVerified: ${error.message} `); + throw error; + } + } + +} diff --git a/apps/user/src/fido/dtos/fido-user.dto.ts b/apps/user/src/fido/dtos/fido-user.dto.ts new file mode 100644 index 000000000..fbf546b67 --- /dev/null +++ b/apps/user/src/fido/dtos/fido-user.dto.ts @@ -0,0 +1,162 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsArray, IsBoolean, IsEmail, IsNotEmpty, IsOptional, IsString, ValidateNested } from 'class-validator'; +export class GenerateRegistrationDto { + @ApiProperty({ example: 'abc@vomoto.com' }) + @IsNotEmpty({ message: 'Email is required.' }) + @IsEmail() + userName: string; + + @IsOptional() + @ApiProperty({ example: 'false' }) + @IsBoolean({ message: 'isPasskey should be boolean' }) + deviceFlag: boolean; +} + +class ResponseDto { + @ApiProperty() + @IsString() + attestationObject: string; + + @ApiProperty() + @IsString() + clientDataJSON: string; + + @ApiProperty() + @IsArray() + transports: string[]; + } + + class ClientExtensionResultsDto { + @ApiProperty() + @ValidateNested() + credProps: Record; + } + + export class VerifyRegistrationDetailsDto { + @ApiProperty() + @IsString() + id: string; + + @ApiProperty() + @IsString() + rawId: string; + + @ApiProperty() + response: ResponseDto; + + @ApiProperty() + @IsString() + type: string; + + @ApiProperty() + clientExtensionResults: ClientExtensionResultsDto; + + @ApiProperty() + @IsString() + authenticatorAttachment: string; + + @ApiProperty() + @IsString() + challangeId: string; + } + + export class VerifyRegistrationPayloadDto { + @ApiProperty() + verifyRegistrationDetails: VerifyRegistrationDetailsDto; + + @ApiProperty() + @IsString() + userName: string; + } + +// +class VerifyAuthenticationResponseDto { + @ApiProperty() + @IsString() + authenticatorData: string; + + @ApiProperty() + @IsString() + clientDataJSON: string; + + @ApiProperty() + @IsString() + signature: string; + + @ApiProperty() + @IsString() + userHandle: string; + } + + export class VerifyAuthenticationDto { + @ApiProperty() + @IsString() + id: string; + + @ApiProperty() + @IsString() + rawId: string; + + @ApiProperty() + response: VerifyAuthenticationResponseDto; + + @ApiProperty() + @IsString() + type: string; + + @ApiProperty() + clientExtensionResults: ClientExtensionResultsDto; + + @ApiProperty() + @IsString() + authenticatorAttachment: string; + + @ApiProperty() + @IsString() + challangeId: string; + } + + export class VerifyAuthenticationPayloadDto { + @ApiProperty() + verifyAuthenticationDetails: VerifyAuthenticationDto; + + @ApiProperty() + @IsString() + userName: string; + } + + export class UpdateFidoUserDetailsDto { + @ApiProperty() + @IsString() + userName: string; + + @ApiProperty() + @IsString() + credentialId: string; + + @ApiProperty() + @IsString() + deviceFriendlyName: string; + } + + export class UserNameDto { + @ApiProperty() + @IsString() + userName: string; + } + + export class credentialDto { + @ApiProperty() + @IsString() + credentialId: string; + } + + export class updateDeviceDto { + @ApiProperty() + @IsString() + credentialId: string; + + @ApiProperty() + @IsString() + deviceName: string; + } \ No newline at end of file diff --git a/apps/user/src/fido/fido.controller.ts b/apps/user/src/fido/fido.controller.ts new file mode 100644 index 000000000..6d3f9f4dd --- /dev/null +++ b/apps/user/src/fido/fido.controller.ts @@ -0,0 +1,86 @@ +import { Controller, Logger } from '@nestjs/common'; +import { MessagePattern } from '@nestjs/microservices'; +import { GenerateRegistrationDto, VerifyRegistrationPayloadDto, VerifyAuthenticationPayloadDto, UpdateFidoUserDetailsDto, UserNameDto, credentialDto, updateDeviceDto } from './dtos/fido-user.dto'; +import { FidoService } from './fido.service'; + +@Controller('fido') +export class FidoController { + constructor(private readonly fidoService: FidoService) { } + private readonly logger = new Logger('PS-Fido-controller'); + + /** + * Description: FIDO User Registration + * @param payload Registration Details + * @returns Get registered user response + */ + @MessagePattern({ cmd: 'generate-registration-options' }) + async generateRegistrationOption(payload: GenerateRegistrationDto): Promise { + return this.fidoService.generateRegistration(payload); + } + + /** + * Description: FIDO User Registration + * @param payload Verify registration + * @returns Get verify registration response + */ + @MessagePattern({ cmd: 'verify-registration' }) + verifyRegistration(payload: VerifyRegistrationPayloadDto): Promise { + return this.fidoService.verifyRegistration(payload); + } + /** + * Description: FIDO User Verification + * @param payload Authentication details + * @returns Get authentication response + */ + @MessagePattern({ cmd: 'generate-authentication-options' }) + generateAuthenticationOption(payload: GenerateRegistrationDto): Promise { + return this.fidoService.generateAuthenticationOption(payload.userName); + } + /** + * Description: FIDO User Verification + * @param payload Verify authentication details + * @returns Get verify authentication details response + */ + @MessagePattern({ cmd: 'verify-authentication' }) + verifyAuthentication(payload: VerifyAuthenticationPayloadDto): Promise { + return this.fidoService.verifyAuthentication(payload); + } + /** + * Description: FIDO User update + * @param payload User Details + * @returns Get updated user detail response + */ + @MessagePattern({ cmd: 'update-user' }) + updateUser(payload: UpdateFidoUserDetailsDto): Promise { + return this.fidoService.updateUser(payload); + } + /** + * Description: fetch FIDO user details + * @param payload User name + * + */ + @MessagePattern({ cmd: 'fetch-fido-user-details' }) + fetchFidoUserDetails(payload: UserNameDto):Promise { + return this.fidoService.fetchFidoUserDetails(payload.userName); + } + + /** + * Description: delete FIDO user details + * @param payload credentialId + * + */ + @MessagePattern({ cmd: 'delete-fido-user-device' }) + deleteFidoUserDevice(payload: credentialDto):Promise { + return this.fidoService.deleteFidoUserDevice(payload); + } + + /** + * Description: update FIDO user details + * @param payload credentialId and deviceName + * + */ + @MessagePattern({ cmd: 'update-fido-user-device-name' }) + updateFidoUserDeviceName(payload: updateDeviceDto):Promise { + return this.fidoService.updateFidoUserDeviceName(payload); + } +} diff --git a/apps/user/src/fido/fido.module.ts b/apps/user/src/fido/fido.module.ts new file mode 100644 index 000000000..bf88e0940 --- /dev/null +++ b/apps/user/src/fido/fido.module.ts @@ -0,0 +1,51 @@ +import { CommonModule } from '@credebl/common'; +import { HttpModule } from '@nestjs/axios'; +import { Module, Logger } from '@nestjs/common'; +import { ClientsModule, Transport } from '@nestjs/microservices'; +import { UserDevicesRepository } from '../../repositories/user-device.repository'; +import { UserRepository } from '../../repositories/user.repository'; +import { FidoController } from './fido.controller'; +import { FidoService } from './fido.service'; +import { PrismaService } from '@credebl/prisma-service'; +import { UserService } from '../user.service'; +import { ClientRegistrationService } from '@credebl/client-registration'; +import { KeycloakUrlService } from '@credebl/keycloak-url'; +import { FidoUserRepository } from '../../repositories/fido-user.repository'; +import { OrgRolesService } from '@credebl/org-roles'; +import { UserOrgRolesService } from '@credebl/user-org-roles'; +import { OrgRolesRepository } from 'libs/org-roles/repositories'; +import { UserOrgRolesRepository } from 'libs/user-org-roles/repositories'; + + +@Module({ + imports: [ + ClientsModule.register([ + { + name: 'NATS_CLIENT', + transport: Transport.NATS, + options: { + servers: [`${process.env.NATS_URL}`] + } + } + ]), + HttpModule, + CommonModule + ], + controllers: [FidoController], + providers: [ + UserService, + PrismaService, + FidoService, + UserRepository, + UserDevicesRepository, + ClientRegistrationService, + Logger, + KeycloakUrlService, + FidoUserRepository, + OrgRolesService, + UserOrgRolesService, + OrgRolesRepository, + UserOrgRolesRepository +] +}) +export class FidoModule { } diff --git a/apps/user/src/fido/fido.service.ts b/apps/user/src/fido/fido.service.ts new file mode 100644 index 000000000..5095826b0 --- /dev/null +++ b/apps/user/src/fido/fido.service.ts @@ -0,0 +1,235 @@ +import { BadRequestException, Injectable, Logger, NotFoundException, InternalServerErrorException } from '@nestjs/common'; //InternalServerErrorException +import { CommonService } from '@credebl/common'; +import { ResponseMessages } from '@credebl/common/response-messages'; +import { RpcException } from '@nestjs/microservices'; +import { FidoUserRepository } from '../../repositories/fido-user.repository'; +import { GenerateRegistrationDto, VerifyRegistrationPayloadDto, VerifyAuthenticationPayloadDto, UpdateFidoUserDetailsDto, credentialDto, updateDeviceDto } from './dtos/fido-user.dto'; +import { UserDevicesRepository } from '../../repositories/user-device.repository'; +import { PrismaService } from '@credebl/prisma-service'; + +@Injectable() +export class FidoService { + private readonly logger = new Logger('PS-Fido-Service'); + constructor( + private readonly fidoUserRepository: FidoUserRepository, + private readonly userDevicesRepository: UserDevicesRepository, + private readonly commonService: CommonService, + private readonly prisma: PrismaService + ) { } + async generateRegistration(payload: GenerateRegistrationDto): Promise { + try { + const { userName, deviceFlag } = payload; + const fidoUser = await this.fidoUserRepository.checkFidoUserExist(userName); + if (!fidoUser && !fidoUser.id) { + throw new NotFoundException(ResponseMessages.user.error.notFound); + } + + if (!fidoUser || true === deviceFlag || false === deviceFlag) { + const generatedOption = await this.generateRegistrationOption(userName); + return generatedOption; + } else if (!fidoUser.isFidoVerified) { + const generatedOption = await this.updateUserRegistrationOption(userName); + return generatedOption; + } else { + throw new BadRequestException(ResponseMessages.fido.error.exists); + } + } catch (error) { + this.logger.error(`Error in generate registration option:::${error}`); + throw new RpcException(error.response); + } + } + + generateRegistrationOption(userName: string): Promise { + const url = `${process.env.FIDO_API_ENDPOINT}/generate-registration-options/?userName=${userName}`; + return this.commonService + .httpGet(url, { headers: { 'Content-Type': 'application/json' } }) + .then(async (response) => { + const { user } = response; + const updateUser = await this.fidoUserRepository.updateUserDetails(userName, [ + {fidoUserId:user.id}, + {username:user.name} + ]); + if (updateUser.fidoUserId === user.id) { + return response; + } else { + throw new InternalServerErrorException(ResponseMessages.fido.error.generateRegistration); + } + }); + } + + updateUserRegistrationOption(userName: string): Promise { + const url = `${process.env.FIDO_API_ENDPOINT}/generate-registration-options/?userName=${userName}`; + return this.commonService + .httpGet(url, { headers: { 'Content-Type': 'application/json' } }) + .then(async (response) => { + const { user } = response; + this.logger.debug(`registration option:: already${JSON.stringify(response)}`); + await this.fidoUserRepository.updateUserDetails(userName, [ + {fidoUserId:user.id}, + {isFidoVerified:false} + ]); + return response; + }); + } + + async verifyRegistration(verifyRegistrationDto: VerifyRegistrationPayloadDto): Promise { + try { + const { verifyRegistrationDetails, userName } = verifyRegistrationDto; + const url = `${process.env.FIDO_API_ENDPOINT}/verify-registration`; + const payload = JSON.stringify(verifyRegistrationDetails); + const response = await this.commonService.httpPost(url, payload, { + headers: { 'Content-Type': 'application/json' } + }); + if (response?.verified && userName) { + await this.fidoUserRepository.updateUserDetails(userName, [{isFidoVerified:true}]); + const credentialID = response.newDevice.credentialID.replace(/=*$/, ''); + response.newDevice.credentialID = credentialID; + const getUser = await this.fidoUserRepository.checkFidoUserExist(userName); + await this.userDevicesRepository.createMultiDevice(response?.newDevice, getUser.id); + return response; + } else { + throw new InternalServerErrorException(ResponseMessages.fido.error.verification); + } + } catch (error) { + this.logger.error(`Error in verify registration option:::${error}`); + throw new RpcException(error); + } + } + + async generateAuthenticationOption(userName: string): Promise { + try { + const fidoUser = await this.fidoUserRepository.checkFidoUserExist(userName); + if (fidoUser && fidoUser.id) { + const fidoMultiDevice = await this.userDevicesRepository.getfidoMultiDevice(fidoUser.id); + const credentialIds = []; + if (fidoMultiDevice) { + for (const iterator of fidoMultiDevice) { + credentialIds.push(iterator.devices.credentialID); + } + } else { + throw new BadRequestException(ResponseMessages.fido.error.deviceNotFound); + } + const url = `${process.env.FIDO_API_ENDPOINT}/generate-authentication-options`; + return await this.commonService + .httpPost(url, credentialIds, { headers: { 'Content-Type': 'application/json' } }) + .then(async (response) => response); + } else { + throw new BadRequestException(ResponseMessages.fido.error.invalidCredentials); + } + } catch (error) { + this.logger.error(`Error in generate authentication option:::${JSON.stringify(error)}`); + throw new RpcException(error.response); + } + } + + async verifyAuthentication(verifyAuthenticationDto: VerifyAuthenticationPayloadDto): Promise { + try { + const { verifyAuthenticationDetails, userName } = verifyAuthenticationDto; + const fidoUser = await this.fidoUserRepository.checkFidoUserExist(userName); + const fidoMultiDevice = await this.userDevicesRepository.getfidoMultiDeviceDetails(fidoUser.id); + const url = `${process.env.FIDO_API_ENDPOINT}/verify-authentication`; + const payload = { verifyAuthenticationDetails: JSON.stringify(verifyAuthenticationDetails), devices: fidoMultiDevice }; + + const credentialIdChars = { + '-': '+', + '_': '/' + }; + + const verifyAuthenticationId = verifyAuthenticationDetails.id.replace(/[-_]/g, replaceCredentialId => credentialIdChars[replaceCredentialId]); + const credentialId = `${verifyAuthenticationId}`; + const getUserDevice = await this.userDevicesRepository.checkUserDeviceByCredentialId(credentialId); + if (getUserDevice) { + const loginCounter = getUserDevice?.authCounter + 1; + if (!payload.devices) { + throw new BadRequestException(ResponseMessages.fido.error.deviceNotFound); + } else { + return await this.commonService + .httpPost(url, payload, { headers: { 'Content-Type': 'application/json' } }) + .then(async (response) => { + if (true === response.verified) { + await this.userDevicesRepository.updateFidoAuthCounter(credentialId, loginCounter); + } + return response; + }); + } + } else { + throw new InternalServerErrorException(ResponseMessages.fido.error.deviceNotFound); + } + } catch (error) { + + this.logger.error(`Error in verify authentication:::${error}`); + throw new RpcException(ResponseMessages.fido.error.deviceNotFound); + } + } + + async updateUser(updateFidoUserDetailsDto: UpdateFidoUserDetailsDto): Promise { + try { + const { deviceFriendlyName, credentialId } = updateFidoUserDetailsDto; + const updateFidoUser = await this.userDevicesRepository.updateDeviceByCredentialId(credentialId); + if (updateFidoUser[0].id) { + await this.userDevicesRepository.addCredentialIdAndNameById(updateFidoUser[0].id, credentialId, deviceFriendlyName); + + } + if (updateFidoUser[0].id) { + return 'User updated.'; + } else { + throw new InternalServerErrorException(ResponseMessages.fido.error.updateFidoUser); + } + + } catch (error) { + this.logger.error(`Error in update user details:::${error}`); + throw new RpcException(error); + } + } + + async fetchFidoUserDetails(userName: string): Promise { + try { + const fidoUser = await this.fidoUserRepository.checkFidoUserExist(userName); + if (!fidoUser) { + throw new NotFoundException(ResponseMessages.user.error.notFound); + } + const multiDevice = await this.userDevicesRepository.fidoMultiDevice(fidoUser.id); + if (multiDevice) { + return multiDevice; + } else { + throw new RpcException(Error); + } + } catch (error) { + this.logger.error(`Error in fetching the user details:::${error}`); + throw new RpcException(error); + } + } + + async deleteFidoUserDevice(payload: credentialDto): Promise { + try { + const { credentialId } = payload; + await this.userDevicesRepository.checkUserDeviceByCredentialId(credentialId); + const deleteUserDevice = await this.userDevicesRepository.deleteUserDeviceByCredentialId(credentialId); + if (1 === deleteUserDevice.count) { + return 'Device deleted successfully'; + } else { + return 'Not deleting this device kindly verify'; + } + } catch (error) { + this.logger.error(`Error in delete user device :::${error}`); + throw new RpcException(error); + } + } + + async updateFidoUserDeviceName(payload: updateDeviceDto): Promise { + try { + const { credentialId, deviceName } = payload; + + const getUserDevice = await this.userDevicesRepository.checkUserDeviceByCredentialId(credentialId); + const updateUserDevice = await this.userDevicesRepository.updateUserDeviceByCredentialId(getUserDevice.id, deviceName); + if (1 === updateUserDevice.count) { + return 'Device name updated successfully.'; + } else { + return 'Device name has not been changed.'; + } + } catch (error) { + this.logger.error(`Error in delete user device :::${error}`); + throw new RpcException(error); + } + } +} diff --git a/apps/user/src/main.ts b/apps/user/src/main.ts new file mode 100644 index 000000000..09b218941 --- /dev/null +++ b/apps/user/src/main.ts @@ -0,0 +1,22 @@ +import { HttpExceptionFilter } from 'libs/http-exception.filter'; +import { Logger } from '@nestjs/common'; +import { NestFactory } from '@nestjs/core'; +import { UserModule } from './user.module'; +import { MicroserviceOptions, Transport } from '@nestjs/microservices'; + +const logger = new Logger(); + +async function bootstrap(): Promise { + const app = await NestFactory.createMicroservice(UserModule, { + transport: Transport.NATS, + options: { + servers: [`${process.env.NATS_URL}`] + } + }); + + app.useGlobalFilters(new HttpExceptionFilter()); + + await app.listen(); + logger.log('User Microservice is listening to NATS '); +} +bootstrap(); diff --git a/apps/user/src/user.controller.ts b/apps/user/src/user.controller.ts new file mode 100644 index 000000000..3cc7baad9 --- /dev/null +++ b/apps/user/src/user.controller.ts @@ -0,0 +1,90 @@ +import { AcceptRejectInvitationDto } from '../dtos/accept-reject-invitation.dto'; +import { Controller } from '@nestjs/common'; +import { LoginUserDto } from '../dtos/login-user.dto'; +import { MessagePattern } from '@nestjs/microservices'; +import { UserService } from './user.service'; +import { VerifyEmailTokenDto } from '../dtos/verify-email.dto'; +import { UserEmailVerificationDto, userInfo } from '../interfaces/user.interface'; + + +@Controller() +export class UserController { + constructor(private readonly userService: UserService) {} + + /** + * Description: Registers new user + * @param payload Registration Details + * @returns Get registered user response + */ + @MessagePattern({ cmd: 'send-verification-mail' }) + async sendVerificationMail(payload: { userEmailVerificationDto: UserEmailVerificationDto }): Promise { + return this.userService.sendVerificationMail(payload.userEmailVerificationDto); + } + + /** + * Description: Verify user's email + * @param param + * @returns Get user's email verified + */ + @MessagePattern({ cmd: 'user-email-verification' }) + async verifyEmail(payload: { param: VerifyEmailTokenDto }): Promise { + return this.userService.verifyEmail(payload.param); + } + + @MessagePattern({ cmd: 'user-holder-login' }) + async login(payload: LoginUserDto): Promise { + return this.userService.login(payload); + } + + @MessagePattern({ cmd: 'get-user-profile' }) + async getProfile(payload: { id }): Promise { + return this.userService.getProfile(payload); + } + + @MessagePattern({ cmd: 'get-user-by-keycloak-id' }) + async findByKeycloakId(payload: { id }): Promise { + return this.userService.findByKeycloakId(payload); + } + + @MessagePattern({ cmd: 'get-user-by-mail' }) + async findUserByEmail(payload: { email }): Promise { + return this.userService.findUserByEmail(payload); + } + + @MessagePattern({ cmd: 'get-org-invitations' }) + async invitations(payload: { id; status; pageNumber; pageSize; search; }): Promise { + return this.userService.invitations(payload); + } + + /** + * + * @param payload + * @returns Organization invitation status fetch-organization-users + */ + @MessagePattern({ cmd: 'accept-reject-invitations' }) + async acceptRejectInvitations(payload: { + acceptRejectInvitation: AcceptRejectInvitationDto; + userId: number; + }): Promise { + return this.userService.acceptRejectInvitations(payload.acceptRejectInvitation, payload.userId); + } + + /** + * + * @param payload + * @returns organization users list + */ + @MessagePattern({ cmd: 'fetch-organization-users' }) + async get(payload: { orgId: number, pageNumber: number, pageSize: number, search: string }): Promise { + return this.userService.get(payload.orgId, payload.pageNumber, payload.pageSize, payload.search); + } + @MessagePattern({ cmd: 'check-user-exist' }) + async checkUserExist(payload: { userEmail: string }): Promise { + return this.userService.checkUserExist(payload.userEmail); + } + @MessagePattern({ cmd: 'add-user' }) + async addUserDetailsInKeyCloak(payload: { userEmail: string, userInfo:userInfo }): Promise { + return this.userService.createUserInKeyCloak(payload.userEmail, payload.userInfo); + } + +} diff --git a/apps/user/src/user.module.ts b/apps/user/src/user.module.ts new file mode 100644 index 000000000..2d908d499 --- /dev/null +++ b/apps/user/src/user.module.ts @@ -0,0 +1,46 @@ +import { Logger, Module } from '@nestjs/common'; +import { OrgRolesModule, OrgRolesService } from '@credebl/org-roles'; +import { ClientsModule, Transport } from '@nestjs/microservices'; +import { CommonModule } from '@credebl/common'; +import { OrgRolesRepository } from 'libs/org-roles/repositories'; +import { PrismaService } from '@credebl/prisma-service'; +import { UserController } from './user.controller'; +import { UserOrgRolesRepository } from 'libs/user-org-roles/repositories'; +import { UserOrgRolesService } from '@credebl/user-org-roles'; +import { UserRepository } from '../repositories/user.repository'; +import { UserService } from './user.service'; +import { ClientRegistrationService } from '@credebl/client-registration'; +import { KeycloakUrlService } from '@credebl/keycloak-url'; +import { FidoModule } from './fido/fido.module'; + + +@Module({ + imports: [ + ClientsModule.register([ + { + name: 'NATS_CLIENT', + transport: Transport.NATS, + options: { + servers: [`${process.env.NATS_URL}`] + } + } + ]), + CommonModule, + FidoModule, + OrgRolesModule + ], + controllers: [UserController], + providers: [ + UserService, + UserRepository, + PrismaService, + Logger, + ClientRegistrationService, + KeycloakUrlService, + OrgRolesService, + UserOrgRolesService, + OrgRolesRepository, + UserOrgRolesRepository + ] +}) +export class UserModule {} diff --git a/apps/user/src/user.service.ts b/apps/user/src/user.service.ts new file mode 100644 index 000000000..603e4f21a --- /dev/null +++ b/apps/user/src/user.service.ts @@ -0,0 +1,516 @@ +import * as bcrypt from 'bcrypt'; + +import { + BadRequestException, + ConflictException, + Injectable, + Logger, + NotFoundException, + UnauthorizedException +} from '@nestjs/common'; + +import { ClientRegistrationService } from '@credebl/client-registration'; +import { CommonService } from '@credebl/common'; +import { EmailDto } from '@credebl/common/dtos/email.dto'; +import { InternalServerErrorException } from '@nestjs/common'; +import { LoginUserDto } from '../dtos/login-user.dto'; +import { OrgRoles } from 'libs/org-roles/enums'; +import { OrgRolesService } from '@credebl/org-roles'; +import { PrismaService } from '@credebl/prisma-service'; +import { ResponseMessages } from '@credebl/common/response-messages'; +import { ClientProxy, RpcException } from '@nestjs/microservices'; +import { URLUserEmailTemplate } from '../templates/user-url-template'; +import { UserOrgRolesService } from '@credebl/user-org-roles'; +import { UserRepository } from '../repositories/user.repository'; +import { VerifyEmailTokenDto } from '../dtos/verify-email.dto'; +import { sendEmail } from '@credebl/common/send-grid-helper-file'; +// eslint-disable-next-line camelcase +import { user } from '@prisma/client'; +import { Inject } from '@nestjs/common'; +import { HttpException } from '@nestjs/common'; +import { InvitationsI, UserEmailVerificationDto, userInfo } from '../interfaces/user.interface'; +import { AcceptRejectInvitationDto } from '../dtos/accept-reject-invitation.dto'; + + +@Injectable() +export class UserService { + constructor( + private readonly prisma: PrismaService, + private readonly clientRegistrationService: ClientRegistrationService, + private readonly commonService: CommonService, + private readonly orgRoleService: OrgRolesService, + private readonly userOrgRoleService: UserOrgRolesService, + private readonly userRepository: UserRepository, + private readonly logger: Logger, + @Inject('NATS_CLIENT') private readonly userServiceProxy: ClientProxy + ) { } + + /** + * + * @param userEmailVerificationDto + * @returns + */ + async sendVerificationMail(userEmailVerificationDto: UserEmailVerificationDto): Promise { + try { + const userDetails = await this.userRepository.checkUserExist(userEmailVerificationDto.email); + + if (userDetails && userDetails.isEmailVerified) { + throw new ConflictException(ResponseMessages.user.error.exists); + } + + if (userDetails && !userDetails.isEmailVerified) { + throw new ConflictException(ResponseMessages.user.error.verificationAlreadySent); + } + + const resUser = await this.userRepository.createUser(userEmailVerificationDto); + + try { + await this.sendEmailForVerification(userEmailVerificationDto.email, resUser.verificationCode); + } catch (error) { + throw new InternalServerErrorException(ResponseMessages.user.error.emailSend); + } + + return resUser; + } catch (error) { + this.logger.error(`In Create User : ${JSON.stringify(error)}`); + throw new RpcException(error.response); + } + } + + /** + * + * @param email + * @param orgName + * @param verificationCode + * @returns + */ + + async sendEmailForVerification(email: string, verificationCode: string): Promise { + try { + const platformConfigData = await this.prisma.platform_config.findMany(); + + const urlEmailTemplate = new URLUserEmailTemplate(); + const emailData = new EmailDto(); + emailData.emailFrom = platformConfigData[0].emailFrom; + emailData.emailTo = email; + emailData.emailSubject = `${process.env.PLATFORM_NAME} Platform: Email Verification`; + + emailData.emailHtml = await urlEmailTemplate.getUserURLTemplate(email, verificationCode, 'USER'); + const isEmailSent = await sendEmail(emailData); + if (isEmailSent) { + return isEmailSent; + } else { + throw new InternalServerErrorException(ResponseMessages.user.error.emailSend); + } + + } catch (error) { + this.logger.error(`error in create keycloak user: ${JSON.stringify(error)}`); + throw new InternalServerErrorException(error.message); + } + } + + + /** + * + * @param param email, verification code + * @returns Email verification succcess + */ + + async verifyEmail(param: VerifyEmailTokenDto): Promise { + try { + const invalidMessage = ResponseMessages.user.error.invalidEmailUrl; + + if (!param.verificationCode || !param.email) { + throw new UnauthorizedException(invalidMessage); + } + + const userDetails = await this.userRepository.getUserDetails(param.email); + + if (!userDetails || param.verificationCode !== userDetails.verificationCode) { + throw new UnauthorizedException(invalidMessage); + } + + if (userDetails.isEmailVerified) { + throw new ConflictException(ResponseMessages.user.error.verifiedEmail); + } + + if (param.verificationCode === userDetails.verificationCode) { + await this.userRepository.verifyUser(param.email); + return { + message: "User Verified sucessfully" + }; + } + } catch (error) { + this.logger.error(`error in verifyEmail: ${JSON.stringify(error)}`); + throw new RpcException(error.response); + } + } + + /** + * + * @param param email, verification code + * @returns Email verification succcess + */ + + async createUserInKeyCloak(email: string, userInfo: userInfo): Promise { + try { + if (!email) { + throw new UnauthorizedException(ResponseMessages.user.error.invalidEmail); + } + const checkUserDetails = await this.userRepository.getUserDetails(email); + if (!checkUserDetails) { + throw new NotFoundException(ResponseMessages.user.error.invalidEmail); + } + if (checkUserDetails.keycloakUserId) { + throw new ConflictException(ResponseMessages.user.error.exists); + } + if (false === checkUserDetails.isEmailVerified) { + throw new NotFoundException(ResponseMessages.user.error.verifyEmail); + } + const resUser = await this.userRepository.updateUserInfo(email, userInfo); + if (!resUser) { + throw new NotFoundException(ResponseMessages.user.error.invalidEmail); + } + const userDetails = await this.userRepository.getUserDetails(email); + + if (!userDetails) { + throw new NotFoundException(ResponseMessages.user.error.adduser); + } + const clientManagementToken = await this.clientRegistrationService.getManagementToken(); + + const keycloakDetails = await this.keycloakUserRegistration(userDetails, clientManagementToken); + + await this.userRepository.updateUserDetails( + userDetails.id, + keycloakDetails + ); + + const holderRoleData = await this.orgRoleService.getRole(OrgRoles.HOLDER); + await this.userOrgRoleService.createUserOrgRole(userDetails.id, holderRoleData.id); + + return "User created successfully"; + } catch (error) { + this.logger.error(`error in create keycloak user: ${JSON.stringify(error)}`); + throw new RpcException(error.response); + } + } + + /** + * + * @param userName + * @param clientToken + * @returns Keycloak client details + */ + async keycloakClienGenerate(userName: string, clientToken: string): Promise<{ clientId; clientSecret }> { + try { + const userClient = await this.clientRegistrationService.createClient(userName, clientToken); + + return userClient; + } catch (error) { + this.logger.error(`error in keycloakClienGenerate: ${JSON.stringify(error)}`); + throw error; + } + } + + /** + * + * @param keycloakUserRegestrationDto + * @returns Email verification succcess + */ + + async keycloakUserRegistration(userDetails: user, clientToken: string): Promise { + const keycloakRegistrationPayload = { + email: userDetails.email, + firstName: userDetails.firstName, + lastName: userDetails.lastName, + username: userDetails.username, + enabled: true, + totp: true, + emailVerified: true, + notBefore: 0, + credentials: [ + { + type: 'password', + value: `${userDetails.password}`, + temporary: false + } + ], + access: { + manageGroupMembership: true, + view: true, + mapRoles: true, + impersonate: true, + manage: true + }, + realmRoles: ['user', 'offline_access'], + attributes: { + uid: [], + homedir: [], + shell: [] + } + }; + + try { + const createUserResponse = await this.clientRegistrationService.registerKeycloakUser( + keycloakRegistrationPayload, + process.env.KEYCLOAK_CREDEBL_REALM, + clientToken + ); + + return createUserResponse?.keycloakUserId; + } catch (error) { + this.logger.error(`error in keycloakUserRegistration: ${JSON.stringify(error)}`); + throw error; + } + } + + /** + * + * @param loginUserDto + * @returns User access token details + */ + async login(loginUserDto: LoginUserDto): Promise { + const { email, password, isPasskey } = loginUserDto; + + try { + const userData = await this.userRepository.checkUserExist(email); + + if (!userData) { + throw new NotFoundException(ResponseMessages.user.error.notFound); + } + + if (userData && !userData.isEmailVerified) { + throw new BadRequestException(ResponseMessages.user.error.verifyMail); + } + + if (true === isPasskey && false === userData?.isFidoVerified) { + throw new UnauthorizedException(ResponseMessages.user.error.registerFido); + } + + if (true === isPasskey && userData?.username && true === userData?.isFidoVerified) { + //Get user token from keycloak + const token = await this.clientRegistrationService.getUserToken(email, userData.password); + return token; + } + + const comparePassword = await bcrypt.compare(password, userData.password); + + if (!comparePassword) { + this.logger.error(`Password Is wrong`); + throw new BadRequestException(ResponseMessages.user.error.invalidCredentials); + } + + //Get user token from kelycloak + const token = await this.clientRegistrationService.getUserToken(email, userData.password); + return token; + } catch (error) { + this.logger.error(`In Login User : ${JSON.stringify(error)}`); + throw new RpcException(error.response); + } + } + + async getProfile(payload: { id }): Promise { + try { + return this.userRepository.getUserById(payload.id); + } catch (error) { + this.logger.error(`get user: ${JSON.stringify(error)}`); + throw new RpcException(error.response); + } + } + + async findByKeycloakId(payload: { id }): Promise { + try { + return this.userRepository.getUserByKeycloakId(payload.id); + } catch (error) { + this.logger.error(`get user: ${JSON.stringify(error)}`); + throw new RpcException(error.response); + } + } + + async findUserByEmail(payload: { email }): Promise { + try { + return this.userRepository.findUserByEmail(payload.email); + } catch (error) { + this.logger.error(`findUserByEmail: ${JSON.stringify(error)}`); + throw new RpcException(error.response); + } + } + + async invitations(payload: { id; status; pageNumber; pageSize; search; }): Promise { + try { + const userData = await this.userRepository.getUserById(payload.id); + + if (!userData) { + throw new NotFoundException(ResponseMessages.user.error.notFound); + } + + const invitationsData = await this.getOrgInvitations( + userData.email, + payload.status, + payload.pageNumber, + payload.pageSize, + payload.search + ); + + const invitations: InvitationsI[] = await this.updateOrgInvitations(invitationsData['invitations']); + invitationsData['invitations'] = invitations; + return invitationsData; + } catch (error) { + this.logger.error(`Error in get invitations: ${JSON.stringify(error)}`); + throw new RpcException(error.response); + } + } + + async getOrgInvitations(email: string, status: string, pageNumber: number, pageSize: number, search = ''): Promise { + const pattern = { cmd: 'fetch-user-invitations' }; + const payload = { + email, status, pageNumber, pageSize, search + }; + + const invitationsData = await this.userServiceProxy + .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 invitationsData; + } + + async updateOrgInvitations(invitations: InvitationsI[]): Promise { + const updatedInvitations = []; + + for (const invitation of invitations) { + const { status, id, organisation, orgId, userId, orgRoles } = invitation; + + const roles = await this.orgRoleService.getOrgRolesByIds(orgRoles as number[]); + + updatedInvitations.push({ + orgRoles: roles, + status, + id, + orgId, + organisation, + userId + }); + } + + return updatedInvitations; + } + + /** + * + * @param acceptRejectInvitation + * @param userId + * @returns Organization invitation status + */ + async acceptRejectInvitations(acceptRejectInvitation: AcceptRejectInvitationDto, userId: number): Promise { + try { + const userData = await this.userRepository.getUserById(userId); + return this.fetchInvitationsStatus(acceptRejectInvitation, userId, userData.email); + } catch (error) { + this.logger.error(`acceptRejectInvitations: ${error}`); + throw new RpcException(error.response); + } + } + + /** + * + * @param acceptRejectInvitation + * @param userId + * @param email + * @returns + */ + async fetchInvitationsStatus( + acceptRejectInvitation: AcceptRejectInvitationDto, + userId: number, + email: string + ): Promise { + try { + const pattern = { cmd: 'update-invitation-status' }; + + const { orgId, invitationId, status } = acceptRejectInvitation; + + const payload = { userId, orgId, invitationId, status, email }; + + const invitationsData = await this.userServiceProxy + .send(pattern, payload) + .toPromise() + .catch((error) => { + this.logger.error(`catch: ${JSON.stringify(error)}`); + throw new HttpException( + { + statusCode: error.statusCode, + error: error.message + }, + error.error + ); + }); + + return invitationsData; + } catch (error) { + this.logger.error(`Error In fetchInvitationsStatus: ${JSON.stringify(error)}`); + throw new RpcException(error.response); + } + } + + /** + * + * @param orgId + * @returns users list + */ + async get(orgId: number, pageNumber: number, pageSize: number, search: string): Promise { + try { + const query = { + userOrgRoles: { + some: { orgId } + }, + OR: [ + { firstName: { contains: search } }, + { lastName: { contains: search } }, + { email: { contains: search } } + ] + }; + + const filterOptions = { + orgId + }; + return this.userRepository.findUsers(query, pageNumber, pageSize, filterOptions); + } catch (error) { + this.logger.error(`get Users: ${JSON.stringify(error)}`); + throw new RpcException(error.response); + } + } + + async checkUserExist(email: string): Promise { + try { + const userDetails = await this.userRepository.checkUniqueUserExist(email); + if (userDetails && !userDetails.isEmailVerified) { + + throw new ConflictException(ResponseMessages.user.error.verificationAlreadySent); + } else if (userDetails && userDetails.keycloakUserId) { + throw new ConflictException(ResponseMessages.user.error.exists); + } else if (null === userDetails) { + return 'New User'; + } else { + const userVerificationDetails = { + isEmailVerified: userDetails.isEmailVerified, + isFidoVerified: userDetails.isFidoVerified, + isKeycloak: null !== userDetails.keycloakUserId && undefined !== userDetails.keycloakUserId + }; + return userVerificationDetails; + } + + } catch (error) { + this.logger.error(`In check User : ${JSON.stringify(error)}`); + throw new RpcException(error.response); + } + } +} diff --git a/apps/user/templates/user-onboard.template.ts b/apps/user/templates/user-onboard.template.ts new file mode 100644 index 000000000..5e8bd62a2 --- /dev/null +++ b/apps/user/templates/user-onboard.template.ts @@ -0,0 +1,85 @@ +export class OnBoardVerificationRequest { + + public getOnBoardUserRequest(name: string, email: string): string { + const year: number = new Date().getFullYear(); + + try { + return ` + + + WELCOME TO CREDEBL + + + + + +
+
+ Credebl Logo +
+
+ Invite Image +
+
+

Hello ${email},

+

The ${name ? name : email} has been sent an onboard request

+

+ In case you need any assistance to access your account, please contact + Blockster Labs +

+
+
+
+ + f + t +
+

+ ® CREDEBL ${year}, Powered by Blockster Labs. All Rights Reserved. +

+
+
+
+ + `; + } catch (error) { + } + } +} \ No newline at end of file diff --git a/apps/user/templates/user-url-template.ts b/apps/user/templates/user-url-template.ts new file mode 100644 index 000000000..b7ab969da --- /dev/null +++ b/apps/user/templates/user-url-template.ts @@ -0,0 +1,81 @@ +import * as url from 'url'; + +export class URLUserEmailTemplate { + + public getUserURLTemplate(email: string, verificationCode: string, type: string): string { + const endpoint = `${process.env.FRONT_END_URL}`; + const year: number = new Date().getFullYear(); + let apiUrl; + + if ('ADMIN' === type) { + apiUrl = url.parse(`${endpoint}/verify-email-success?verificationCode=${verificationCode}&email=${encodeURIComponent(email)}`); + } else { + apiUrl = url.parse(`${endpoint}/verify-email-success?verificationCode=${verificationCode}&email=${encodeURIComponent(email)}`); + } + + const validUrl = apiUrl.href.replace('/:', ':'); + try { + return ` + + + + + + + + + +
+
+ Credebl Logo +
+
+ verification Image +
+
+

+ Hello ${email} , +

+

+ Your user account ${email} has been successfully created on ${process.env.PLATFORM_NAME}. In order to enable access for your account, + we need to verify your email address. Please use the link below or click on the “Verify” button to enable access to your account. +

+

Your account details as follows,

+
    +
  • Username/Email: ${email}
  • +
  • Verification Link: ${validUrl}
  • +
+ +

In case you need any assistance to access your account, please contact Blockster Labs +

+
+
+
+ + f + t +
+

+ ® CREDEBL ${year}, Powered by Blockster Labs. All Rights Reserved. +

+
+
+
+ + `; + + } catch (error) { + } + } +} + diff --git a/apps/user/test/app.e2e-spec.ts b/apps/user/test/app.e2e-spec.ts new file mode 100644 index 000000000..ab51d4a94 --- /dev/null +++ b/apps/user/test/app.e2e-spec.ts @@ -0,0 +1,24 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { INestApplication } from '@nestjs/common'; +import * as request from 'supertest'; +import { UserModule } from './../src/user.module'; + +describe('UserController (e2e)', () => { + let app: INestApplication; + + beforeEach(async () => { + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [UserModule] + }).compile(); + + app = moduleFixture.createNestApplication(); + await app.init(); + }); + + it('/ (GET)', () => { + return request(app.getHttpServer()) + .get('/') + .expect(200) + .expect('Hello World!'); + }); +}); diff --git a/apps/user/test/jest-e2e.json b/apps/user/test/jest-e2e.json new file mode 100644 index 000000000..e9d912f3e --- /dev/null +++ b/apps/user/test/jest-e2e.json @@ -0,0 +1,9 @@ +{ + "moduleFileExtensions": ["js", "json", "ts"], + "rootDir": ".", + "testEnvironment": "node", + "testRegex": ".e2e-spec.ts$", + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + } +} diff --git a/apps/user/tsconfig.app.json b/apps/user/tsconfig.app.json new file mode 100644 index 000000000..118b50673 --- /dev/null +++ b/apps/user/tsconfig.app.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "declaration": false, + "outDir": "../../dist/apps/user" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] +} diff --git a/apps/verification/Dockerfile b/apps/verification/Dockerfile new file mode 100644 index 000000000..b997a7c3f --- /dev/null +++ b/apps/verification/Dockerfile @@ -0,0 +1,43 @@ +# Stage 1: Build the application +FROM node:16-alpine as build + +# Set the working directory +WORKDIR /app + +# Copy package.json and package-lock.json +COPY package.json ./ +#COPY package-lock.json ./ + +# Install dependencies +RUN npm i + +# Copy the rest of the application code +COPY . . +CMD ["sh", "-c", "cd libs/prisma-service && npx prisma generate"] + +# Build the user service +RUN npm run build verification + +# Stage 2: Create the final image +FROM node:16-alpine + +# Set the working directory +WORKDIR /app + +# Copy the compiled code from the build stage +COPY --from=build /app/dist/apps/verification/ ./dist/apps/verification/ + +# 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 + +# Install production dependencies for the final image +#RUN npm i --only=production + +# Set the command to run the microservice +CMD ["sh", "-c", "cd libs/prisma-service && npx prisma generate && cd ../.. && node dist/apps/verification/main.js"] + +# docker build -t verification -f apps/verification/Dockerfile . +# docker run -d --env-file .env --name verification docker.io/library/verification +# docker logs -f verification diff --git a/apps/verification/src/interfaces/verification.interface.ts b/apps/verification/src/interfaces/verification.interface.ts new file mode 100644 index 000000000..5b351dd83 --- /dev/null +++ b/apps/verification/src/interfaces/verification.interface.ts @@ -0,0 +1,99 @@ + +interface IProofRequestAttribute { + attributeName: string; + condition?: string; + value?: string; + credDefId: string; + credentialName: string; +} + +export interface IRequestProof { + orgId: number; + connectionId: string; + attributes: IProofRequestAttribute[]; + comment: string; + autoAcceptProof: string; + protocolVersion: string; +} + +export interface IGetAllProofPresentations { + url: string; + apiKey: string; +} + +export interface IGetProofPresentationById { + url: string; + apiKey: string; +} + +export interface IVerifyPresentation { + url: string; + apiKey: string; +} + +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; + restrictions: IRequestedRestriction[] +} + +interface IRequestedPredicates { + [key: string]: IRequestedPredicatesName; +} + +interface IRequestedPredicatesName { + name: string; + restrictions: IRequestedRestriction[] +} + +interface IRequestedRestriction { + cred_def_id: string; +} + +export interface ISendProofRequestPayload { + protocolVersion: string; + comment: string; + connectionId: string; + proofFormats: IProofFormats; + autoAcceptProof: string; +} + +export interface IProofRequestPayload { + url: string; + apiKey: string; + proofRequestPayload: ISendProofRequestPayload +} + +interface IWebhookPresentationProof { + threadId: string; + state: string; + connectionId +} + +export interface IWebhookProofPresentation { + metadata: object; + _tags: IWebhookPresentationProof; + id: string; + createdAt: string; + protocolVersion: string; + state: string; + connectionId: string; + threadId: string; + autoAcceptProof: string; + updatedAt: string; + isVerified: boolean; +} diff --git a/apps/verification/src/main.ts b/apps/verification/src/main.ts new file mode 100644 index 000000000..e3b1a968a --- /dev/null +++ b/apps/verification/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 { VerificationModule } from './verification.module'; + +const logger = new Logger(); + +async function bootstrap(): Promise { + + const app = await NestFactory.createMicroservice(VerificationModule, { + transport: Transport.NATS, + options: { + servers: [`${process.env.NATS_URL}`] + } + }); + + app.useGlobalFilters(new HttpExceptionFilter()); + + await app.listen(); + logger.log('Verification-Service Microservice is listening to NATS '); +} +bootstrap(); diff --git a/apps/verification/src/repositories/verification.repository.ts b/apps/verification/src/repositories/verification.repository.ts new file mode 100644 index 000000000..4780f8ba6 --- /dev/null +++ b/apps/verification/src/repositories/verification.repository.ts @@ -0,0 +1,66 @@ +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, presentations } from "@prisma/client"; +import { IWebhookProofPresentation } from "../interfaces/verification.interface"; + + +@Injectable() +export class VerificationRepository { + constructor(private readonly prisma: PrismaService, private readonly logger: Logger) { } + + /** + * Get org agent details + * @param orgId + * @returns + */ + // eslint-disable-next-line camelcase + async getAgentEndPoint(orgId: number): Promise { + try { + + const agentDetails = await this.prisma.org_agents.findFirst({ + where: { + orgId + } + }); + + if (!agentDetails) { + throw new NotFoundException(ResponseMessages.verification.error.notFound); + } + + return agentDetails; + + } catch (error) { + this.logger.error(`[getProofPresentations] - error in get agent endpoint : ${error.message} `); + throw error; + } + } + + async storeProofPresentation(id: string, proofPresentationPayload: IWebhookProofPresentation): Promise { + try { + + return await this.prisma.presentations.upsert({ + where: { + connectionId: proofPresentationPayload.connectionId + }, + update: { + state: proofPresentationPayload.state, + threadId: proofPresentationPayload.threadId, + isVerified: proofPresentationPayload.isVerified + }, + create: { + connectionId: proofPresentationPayload.connectionId, + state: proofPresentationPayload.state, + threadId: proofPresentationPayload.threadId, + isVerified: proofPresentationPayload.isVerified, + orgId: parseInt(id) + } + }); + + } catch (error) { + this.logger.error(`[getProofPresentations] - error in get agent endpoint : ${error.message} `); + throw error; + } + } +} \ No newline at end of file diff --git a/apps/verification/src/verification.controller.ts b/apps/verification/src/verification.controller.ts new file mode 100644 index 000000000..565d91fc3 --- /dev/null +++ b/apps/verification/src/verification.controller.ts @@ -0,0 +1,56 @@ +import { Controller } from '@nestjs/common'; +import { VerificationService } from './verification.service'; +import { MessagePattern } from '@nestjs/microservices'; +import { IRequestProof, IWebhookProofPresentation } from './interfaces/verification.interface'; +import { IUserRequest } from '@credebl/user-request/user-request.interface'; +import { presentations } from '@prisma/client'; + +@Controller() +export class VerificationController { + constructor(private readonly verificationService: VerificationService) { } + + /** + * Get all proof presentations + * @param payload + * @returns Get all proof presentation + */ + @MessagePattern({ cmd: 'get-proof-presentations' }) + async getProofPresentations(payload: { user: IUserRequest, threadId: string, orgId: number }): Promise { + return this.verificationService.getProofPresentations(payload.orgId, payload.threadId); + } + + /** + * Get proof presentation by id + * @param payload + * @returns Get proof presentation details + */ + @MessagePattern({ cmd: 'get-proof-presentations-by-id' }) + async getProofPresentationById(payload: { id: string, orgId: number, user: IUserRequest }): Promise { + return this.verificationService.getProofPresentationById(payload.id, payload.orgId); + } + + /** + * Request proof presentation + * @param payload + * @returns Get requested proof presentation details + */ + @MessagePattern({ cmd: 'send-proof-request' }) + async sendProofRequest(payload: { requestProof: IRequestProof, user: IUserRequest }): Promise { + return this.verificationService.sendProofRequest(payload.requestProof); + } + + /** + * Verify proof presentation + * @param payload + * @returns Get verified proof presentation details + */ + @MessagePattern({ cmd: 'verify-presentation' }) + async verifyPresentation(payload: { id: string, orgId: number, user: IUserRequest }): Promise { + return this.verificationService.verifyPresentation(payload.id, payload.orgId); + } + + @MessagePattern({ cmd: 'webhook-proof-presentation' }) + async webhookProofPresentation(payload: { id: string, proofPresentationPayload: IWebhookProofPresentation }): Promise { + return this.verificationService.webhookProofPresentation(payload.id, payload.proofPresentationPayload); + } +} diff --git a/apps/verification/src/verification.module.ts b/apps/verification/src/verification.module.ts new file mode 100644 index 000000000..4b6ea652f --- /dev/null +++ b/apps/verification/src/verification.module.ts @@ -0,0 +1,26 @@ +import { Logger, Module } from '@nestjs/common'; +import { VerificationController } from './verification.controller'; +import { VerificationService } from './verification.service'; +import { ClientsModule, Transport } from '@nestjs/microservices'; +import { CommonModule } from '@credebl/common'; +import { VerificationRepository } from './repositories/verification.repository'; +import { PrismaService } from '@credebl/prisma-service'; + +@Module({ + imports: [ + ClientsModule.register([ + { + name: 'NATS_CLIENT', + transport: Transport.NATS, + options: { + servers: [`${process.env.NATS_URL}`] + } + } + ]), + + CommonModule + ], + controllers: [VerificationController], + providers: [VerificationService, VerificationRepository, PrismaService, Logger] +}) +export class VerificationModule { } diff --git a/apps/verification/src/verification.service.ts b/apps/verification/src/verification.service.ts new file mode 100644 index 000000000..355424616 --- /dev/null +++ b/apps/verification/src/verification.service.ts @@ -0,0 +1,450 @@ +/* eslint-disable camelcase */ +import { HttpException, Inject, Injectable, Logger, NotFoundException } from '@nestjs/common'; +import { ClientProxy, RpcException } from '@nestjs/microservices'; +import { map } from 'rxjs/operators'; +import { IGetAllProofPresentations, IGetProofPresentationById, IProofRequestPayload, IRequestProof, ISendProofRequestPayload, IVerifyPresentation, IWebhookProofPresentation } from './interfaces/verification.interface'; +import { VerificationRepository } from './repositories/verification.repository'; +import { CommonConstants } from '@credebl/common/common.constant'; +import { presentations } from '@prisma/client'; +import { OrgAgentType } from '@credebl/enum/enum'; +import { ResponseMessages } from '@credebl/common/response-messages'; + +@Injectable() +export class VerificationService { + + private readonly logger = new Logger('VerificationService'); + + constructor( + @Inject('NATS_CLIENT') private readonly verificationServiceProxy: ClientProxy, + private readonly verificationRepository: VerificationRepository + + ) { } + + /** + * Get all proof presentations + * @param user + * @param orgId + * @returns Get all proof presentation + */ + async getProofPresentations(orgId: number, threadId: string): Promise { + try { + const getAgentDetails = await this.verificationRepository.getAgentEndPoint(orgId); + + const verificationMethodLabel = 'get-proof-presentation'; + let url; + if (threadId) { + url = await this.getAgentUrl(verificationMethodLabel, getAgentDetails?.orgAgentTypeId, getAgentDetails?.agentEndPoint, getAgentDetails?.tenantId, threadId); + } else { + url = await this.getAgentUrl(verificationMethodLabel, getAgentDetails?.orgAgentTypeId, getAgentDetails?.agentEndPoint, getAgentDetails?.tenantId); + } + + const payload = { apiKey: getAgentDetails.apiKey, url }; + const getProofPresentationsDetails = await this._getProofPresentations(payload); + return getProofPresentationsDetails?.response; + + } catch (error) { + this.logger.error(`[getProofPresentations] - error in get proof presentation : ${JSON.stringify(error)}`); + throw new RpcException(error); + } + } + + /** + * Consume agent API for get all proof presentations + * @param payload + * @returns Get all proof presentation + */ + async _getProofPresentations(payload: IGetAllProofPresentations): Promise<{ + response: string; + }> { + try { + + const pattern = { + cmd: 'agent-get-proof-presentations' + }; + + return this.verificationServiceProxy + .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(`[_getProofPresentations] - error in get proof presentations : ${JSON.stringify(error)}`); + throw error; + } + } + + /** + * Get proof presentation by id + * @param id + * @param orgId + * @param user + * @returns Get proof presentation details + */ + async getProofPresentationById(id: string, orgId: number): Promise { + try { + const getAgentDetails = await this.verificationRepository.getAgentEndPoint(orgId); + + const verificationMethodLabel = 'get-proof-presentation-by-id'; + const url = await this.getAgentUrl(verificationMethodLabel, getAgentDetails?.orgAgentTypeId, getAgentDetails?.agentEndPoint, getAgentDetails?.tenantId, '', id); + + const payload = { apiKey: '', url }; + + const getProofPresentationById = await this._getProofPresentationById(payload); + return getProofPresentationById?.response; + } catch (error) { + this.logger.error(`[getProofPresentationById] - error in get proof presentation by id : ${JSON.stringify(error)}`); + throw new RpcException(error); + } + } + + /** + * Consume agent API for get proof presentation by id + * @param payload + * @returns Get proof presentation details + */ + async _getProofPresentationById(payload: IGetProofPresentationById): Promise<{ + response: string; + }> { + try { + + const pattern = { + cmd: 'agent-get-proof-presentation-by-id' + }; + + return this.verificationServiceProxy + .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(`[_getProofPresentationById] - error in get proof presentation by id : ${JSON.stringify(error)}`); + throw error; + } + } + + /** + * Request proof presentation + * @param requestProof + * @param user + * @returns Get requested proof presentation details + */ + async sendProofRequest(requestProof: IRequestProof): Promise { + try { + let requestedAttributes = {}; + const requestedPredicates = {}; + const comment = requestProof.comment ? requestProof.comment : ''; + + let proofRequestPayload: ISendProofRequestPayload = { + protocolVersion: '', + comment: '', + connectionId: '', + proofFormats: { + indy: { + name: '', + requested_attributes: {}, + requested_predicates: {}, + version: '' + } + }, + autoAcceptProof: '' + }; + + requestedAttributes = requestProof.attributes.reduce((acc, attribute, index) => { + const attributeElement = attribute.attributeName; + const attributeReferent = `additionalProp${index + 1}`; + + if (!attribute.condition && !attribute.value) { + + const keys = Object.keys(acc); + if (0 < keys.length) { + + let attributeFound = false; + for (const attr in keys) { + + if (keys.hasOwnProperty(attr)) { + + if ( + requestedAttributes[attr].restrictions[0].cred_def_id === + requestProof.attributes[index].credDefId + ) { + + requestedAttributes[attr].name.push(attributeElement); + attributeFound = true; + } + if ( + attr === Object.keys(keys)[Object.keys(keys).length - 1] && + !attributeFound + ) { + + requestedAttributes[attributeReferent] = { + name: attributeElement, + restrictions: [ + { + cred_def_id: requestProof.attributes[index].credDefId + } + ] + }; + } + } + } + } else { + + acc[attributeReferent] = { + name: attributeElement, + restrictions: [ + { + cred_def_id: attribute.credDefId + } + ] + }; + } + } else { + requestedPredicates[attributeReferent] = { + p_type: attribute.condition, + restrictions: [ + { + cred_def_id: attribute.credDefId + } + ], + name: attributeElement, + p_value: parseInt(attribute.value) + }; + } + + return acc; + }, {}); + + proofRequestPayload = { + protocolVersion: requestProof.protocolVersion ? requestProof.protocolVersion : 'v1', + comment, + connectionId: requestProof.connectionId, + proofFormats: { + indy: { + name: 'Proof Request', + version: '1.0', + // eslint-disable-next-line camelcase + requested_attributes: requestedAttributes, + // eslint-disable-next-line camelcase + requested_predicates: requestedPredicates + } + }, + autoAcceptProof: requestProof.autoAcceptProof ? requestProof.autoAcceptProof : 'never' + }; + + const getAgentDetails = await this.verificationRepository.getAgentEndPoint(requestProof.orgId); + + const verificationMethodLabel = 'request-proof'; + const url = await this.getAgentUrl(verificationMethodLabel, getAgentDetails?.orgAgentTypeId, getAgentDetails?.agentEndPoint, getAgentDetails?.tenantId); + + const payload = { apiKey: '', url, proofRequestPayload }; + + const getProofPresentationById = await this._sendProofRequest(payload); + return getProofPresentationById?.response; + } catch (error) { + this.logger.error(`[verifyPresentation] - error in verify presentation : ${JSON.stringify(error)}`); + throw new RpcException(error); + } + } + + /** + * Consume agent API for request proof presentation + * @param payload + * @returns Get requested proof presentation details + */ + async _sendProofRequest(payload: IProofRequestPayload): Promise<{ + response: string; + }> { + try { + + const pattern = { + cmd: 'agent-send-proof-request' + }; + + return this.verificationServiceProxy + .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(`[_verifyPresentation] - error in verify presentation : ${JSON.stringify(error)}`); + throw error; + } + } + + /** + * Verify proof presentation + * @param id + * @param orgId + * @param user + * @returns Get verified proof presentation details + */ + async verifyPresentation(id: string, orgId: number): Promise { + try { + const getAgentDetails = await this.verificationRepository.getAgentEndPoint(orgId); + const verificationMethodLabel = 'accept-presentation'; + const url = await this.getAgentUrl(verificationMethodLabel, getAgentDetails?.orgAgentTypeId, getAgentDetails?.agentEndPoint, getAgentDetails?.tenantId, '', id); + + const payload = { apiKey: '', url }; + const getProofPresentationById = await this._verifyPresentation(payload); + return getProofPresentationById?.response; + } catch (error) { + this.logger.error(`[verifyPresentation] - error in verify presentation : ${JSON.stringify(error)}`); + throw new RpcException(error); + } + } + + /** + * Consume agent API for verify proof presentation + * @param payload + * @returns Get verified proof presentation details + */ + async _verifyPresentation(payload: IVerifyPresentation): Promise<{ + response: string; + }> { + try { + + const pattern = { + cmd: 'agent-verify-presentation' + }; + + return this.verificationServiceProxy + .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(`[_verifyPresentation] - error in verify presentation : ${JSON.stringify(error)}`); + throw error; + } + } + + async webhookProofPresentation(id: string, proofPresentationPayload: IWebhookProofPresentation): Promise { + try { + + const proofPresentation = await this.verificationRepository.storeProofPresentation(id, proofPresentationPayload); + return proofPresentation; + + } catch (error) { + this.logger.error(`[webhookProofPresentation] - error in webhook proof presentation : ${JSON.stringify(error)}`); + throw new RpcException(error); + } + } + + /** + * Description: Fetch agent url + * @param referenceId + * @returns agent URL + */ + async getAgentUrl( + verificationMethodLabel: string, + orgAgentTypeId: number, + agentEndPoint: string, + tenantId: string, + threadId?: string, + proofPresentationId?: string + ): Promise { + try { + + let url; + switch (verificationMethodLabel) { + case 'get-proof-presentation': { + url = orgAgentTypeId === OrgAgentType.DEDICATED && threadId + ? `${agentEndPoint}${CommonConstants.URL_GET_PROOF_PRESENTATIONS}?threadId=${threadId}` + : orgAgentTypeId === OrgAgentType.SHARED && threadId + ? `${agentEndPoint}${CommonConstants.URL_SHAGENT_GET_PROOFS}?threadId=${threadId}`.replace('#', tenantId) + : orgAgentTypeId === OrgAgentType.DEDICATED + ? `${agentEndPoint}${CommonConstants.URL_GET_PROOF_PRESENTATIONS}` + : orgAgentTypeId === OrgAgentType.SHARED + ? `${agentEndPoint}${CommonConstants.URL_SHAGENT_GET_PROOFS}`.replace('#', tenantId) + : null; + break; + } + + case 'get-proof-presentation-by-id': { + url = orgAgentTypeId === OrgAgentType.DEDICATED + ? `${agentEndPoint}${CommonConstants.URL_GET_PROOF_PRESENTATION_BY_ID}`.replace('#', proofPresentationId) + : orgAgentTypeId === OrgAgentType.SHARED + ? `${agentEndPoint}${CommonConstants.URL_SHAGENT_GET_PROOFS_BY_PRESENTATION_ID}`.replace('#', proofPresentationId).replace('@', tenantId) + : null; + break; + } + + case 'request-proof': { + url = orgAgentTypeId === OrgAgentType.DEDICATED + ? `${agentEndPoint}${CommonConstants.URL_SEND_PROOF_REQUEST}` + : orgAgentTypeId === OrgAgentType.SHARED + ? `${agentEndPoint}${CommonConstants.URL_SHAGENT_REQUEST_PROOF}`.replace('#', tenantId) + : null; + break; + } + + case 'accept-presentation': { + url = orgAgentTypeId === OrgAgentType.DEDICATED + ? `${agentEndPoint}${CommonConstants.URL_VERIFY_PRESENTATION}`.replace('#', proofPresentationId) + : orgAgentTypeId === OrgAgentType.SHARED + ? `${agentEndPoint}${CommonConstants.URL_SHAGENT_ACCEPT_PRESENTATION}`.replace('@', proofPresentationId).replace('#', tenantId) + : null; + break; + } + + default: { + break; + } + } + + if (!url) { + throw new NotFoundException(ResponseMessages.issuance.error.agentUrlNotFound); + } + + return url; + } catch (error) { + this.logger.error(`Error in get agent url: ${JSON.stringify(error)}`); + throw error; + + } + } +} diff --git a/apps/verification/tsconfig.app.json b/apps/verification/tsconfig.app.json new file mode 100644 index 000000000..3f4a3d9c6 --- /dev/null +++ b/apps/verification/tsconfig.app.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "declaration": false, + "outDir": "../../dist/apps/verification" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..ce5b39140 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,23 @@ +version: '3' + +services: + nats: + container_name: nats + entrypoint: '/nats-server -c nats-server.conf -DV' + image: nats + ports: + - '4222:4222' + - '6222:6222' + - '8222:8222' + redis: + image: redis:6.2-alpine + restart: always + ports: + - '6379:6379' + command: redis-server --save 20 1 --loglevel warning + volumes: + - cache:/data + +volumes: + cache: + driver: local \ No newline at end of file diff --git a/libs/client-registration/src/client-registration.module.ts b/libs/client-registration/src/client-registration.module.ts new file mode 100644 index 000000000..9fd72ef70 --- /dev/null +++ b/libs/client-registration/src/client-registration.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { ClientRegistrationService } from './client-registration.service'; + +@Module({ + providers: [ClientRegistrationService], + exports: [ClientRegistrationService] +}) +export class ClientRegistrationModule {} diff --git a/libs/client-registration/src/client-registration.service.spec.ts b/libs/client-registration/src/client-registration.service.spec.ts new file mode 100644 index 000000000..7d4d230b7 --- /dev/null +++ b/libs/client-registration/src/client-registration.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ClientRegistrationService } from './client-registration.service'; + +describe('ClientRegistrationService', () => { + let service: ClientRegistrationService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ClientRegistrationService] + }).compile(); + + service = module.get(ClientRegistrationService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/libs/client-registration/src/client-registration.service.ts b/libs/client-registration/src/client-registration.service.ts new file mode 100644 index 000000000..3a85950b0 --- /dev/null +++ b/libs/client-registration/src/client-registration.service.ts @@ -0,0 +1,661 @@ + +import { + Injectable, + Logger, + NotFoundException, + UnauthorizedException +} from '@nestjs/common'; +import * as qs from 'qs'; + +import { ClientCredentialTokenPayloadDto } from './dtos/client-credential-token-payload.dto'; +import { CommonConstants } from '@credebl/common/common.constant'; +import { CommonService } from '@credebl/common'; +import { CreateUserDto } from './dtos/create-user.dto'; +import { JwtService } from '@nestjs/jwt'; +import { KeycloakUrlService } from '@credebl/keycloak-url'; +import { accessTokenPayloadDto } from './dtos/accessTokenPayloadDto'; +import { userTokenPayloadDto } from './dtos/userTokenPayloadDto'; +import { KeycloakUserRegistrationDto } from 'apps/user/dtos/keycloak-register.dto'; +import { ResponseMessages } from '@credebl/common/response-messages'; + +@Injectable() +export class ClientRegistrationService { + constructor(private readonly commonService: CommonService, + private readonly keycloakUrlService: KeycloakUrlService) { } + + private readonly logger = new Logger('ClientRegistrationService'); + + async registerKeycloakUser( + userDetails: KeycloakUserRegistrationDto, + realm: string, + token: string + ) { + try { + const url = await this.keycloakUrlService.createUserURL(realm); + const registerUserResponse = await this.commonService.httpPost( + url, + userDetails, + this.getAuthHeader(token) + ); + + const getUserResponse = await this.commonService.httpGet( + await this.keycloakUrlService.getUserByUsernameURL(realm, userDetails.email), + this.getAuthHeader(token) + ); + if (getUserResponse[0].username === userDetails.email || getUserResponse[1].username === userDetails.email) { + return { keycloakUserId: getUserResponse[0].id }; + } else { + throw new NotFoundException(ResponseMessages.user.error.invalidKeycloakId); + } + + } catch (error) { + this.logger.error(`error in keycloakUserRegistration in client-registration: ${JSON.stringify(error)}`); + throw error; + } + + } + + async createUser( + user: CreateUserDto, + realm: string, + token: string + ) { + const payload = { + createdTimestamp: Date.parse(Date.now.toString()), + username: user.email, + enabled: true, + totp: false, + emailVerified: true, + firstName: user.firstName, + lastName: user.lastName, + email: user.email, + disableableCredentialTypes: [], + requiredActions: [], + notBefore: 0, + access: { + manageGroupMembership: true, + view: true, + mapRoles: true, + impersonate: true, + manage: true + }, + realmRoles: ['mb-user'] + }; + + const registerUserResponse = await this.commonService.httpPost( + await this.keycloakUrlService.createUserURL(realm), + payload, + this.getAuthHeader(token) + ); + + const getUserResponse = await this.commonService.httpGet( + await this.keycloakUrlService.getUserByUsernameURL(realm, user.email), + this.getAuthHeader(token) + ); + const userid = getUserResponse[0].id; + + + const setPasswordResponse = await this.resetPasswordOfKeycloakUser(realm, user.password, userid, token); + + return { + keycloakUserId: getUserResponse[0].id + }; + } + + async resetPasswordOfKeycloakUser( + realm: string, + resetPasswordValue: string, + userid: string, + token: string + + ) { + + const passwordPayload = { + type: 'password', + value: resetPasswordValue, + temporary: false + }; + const setPasswordResponse = await this.commonService.httpPut( + //await this.keycloakUrlService.ResetPasswordURL(`${process.env.KEYCLOAK_CREDEBL_REALM}`, userid), + await this.keycloakUrlService.ResetPasswordURL(realm, userid), + passwordPayload, + this.getAuthHeader(token) + ); + return setPasswordResponse; + + } + + getAuthHeader(token: string) { + return { headers: { authorization: `Bearer ${token}` } }; + } + + async getUserInfo(token: string) { + try { + + const jwtService = new JwtService({}); + const decoded = jwtService.decode(token, { complete: true }); + if (!decoded) { + throw new UnauthorizedException( + 'Invalid token' + ); + } + + const payload = decoded['payload']; + + const userInfoResponse = await this.commonService.httpGet( + `${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)}`); + throw error; + } + } + + async getManagementToken() { + try { + 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) { + + throw error; + } + } + + + async getManagementTokenForMobile() { + try { + const payload = new ClientCredentialTokenPayloadDto(); + payload.client_id = process.env.KEYCLOAK_MANAGEMENT_ADEYA_CLIENT_ID; + payload.client_secret = process.env.KEYCLOAK_MANAGEMENT_ADEYA_CLIENT_SECRET; + payload.scope = 'email profile'; + + this.logger.log(`management Payload: ${JSON.stringify(payload)}`); + const mgmtTokenResponse = await this.getToken(payload); + this.logger.debug( + `ClientRegistrationService management token ${JSON.stringify( + mgmtTokenResponse + )}` + ); + //return mgmtTokenResponse; + return mgmtTokenResponse; + } catch (error) { + + throw error; + } + } + + async getClientIdAndSecret( + clientId: string, + token: string + ): Promise<{ clientId: string; clientSecret: string }> | undefined { + // Client id cannot be undefined + if (!clientId) { + return; + } + try { + const realmName = process.env.KEYCLOAK_CREDEBL_REALM; + const getClientResponse = await this.commonService.httpGet( + await this.keycloakUrlService.GetClientURL(realmName, clientId), + this.getAuthHeader(token) + ); + const { id } = getClientResponse[0]; + const client_id = getClientResponse[0].clientId; + + const response = await this.commonService.httpGet( + `${process.env.KEYCLOAK_DOMAIN + }${CommonConstants.URL_KEYCLOAK_CLIENT_SECRET.replace( + '{id}', + id + )}`, + this.getAuthHeader(token) + ); + + this.logger.debug(`Existing apps response ${JSON.stringify(response)}`); + + return { + clientId: client_id, + clientSecret: response.value + }; + } catch (error) { + if (404 === error?.response?.statusCode) { + + } else { + this.logger.error( + `Caught exception while retrieving clientSecret from Auth0: ${JSON.stringify( + error + )}` + ); + throw new Error('Unable to retrieve clientSecret from server'); + } + } + } + + + async createClient( + name: string, + token: string + ) { + + //create client for respective created realm in order to access its resources + const realmName = process.env.KEYCLOAK_CREDEBL_REALM; + const clientPayload = { + clientId: `admin-${name}`, + name: `admin-${name}`, + adminUrl: process.env.KEYCLOAK_ADMIN_URL, + alwaysDisplayInConsole: false, + access: { + view: true, + configure: true, + manage: true + }, + attributes: {}, + authenticationFlowBindingOverrides: {}, + authorizationServicesEnabled: false, + bearerOnly: false, + directAccessGrantsEnabled: true, + enabled: true, + protocol: 'openid-connect', + description: 'rest-api', + + rootUrl: '${authBaseUrl}', + baseUrl: `/realms/${realmName}/account/`, + surrogateAuthRequired: false, + clientAuthenticatorType: 'client-secret', + defaultRoles: [ + 'manage-account', + 'view-profile' + ], + redirectUris: [`/realms/${realmName}/account/*`], + webOrigins: [], + notBefore: 0, + consentRequired: false, + standardFlowEnabled: true, + implicitFlowEnabled: false, + serviceAccountsEnabled: true, + publicClient: false, + frontchannelLogout: false, + fullScopeAllowed: false, + nodeReRegistrationTimeout: 0, + defaultClientScopes: [ + 'web-origins', + 'role_list', + 'profile', + 'roles', + 'email' + ], + optionalClientScopes: [ + 'address', + 'phone', + 'offline_access', + 'microprofile-jwt' + ] + }; + + const createClientResponse = await this.commonService.httpPost( + await this.keycloakUrlService.createClientURL(realmName), + clientPayload, + this.getAuthHeader(token) + ); + this.logger.debug( + `ClientRegistrationService create realm client ${JSON.stringify( + createClientResponse + )}` + ); + + const getClientResponse = await this.commonService.httpGet( + await this.keycloakUrlService.GetClientURL(realmName, `admin-${name}`), + this.getAuthHeader(token) + ); + this.logger.debug( + `ClientRegistrationService get realm admin client ${JSON.stringify( + createClientResponse + )}` + ); + const { id } = getClientResponse[0]; + const client_id = getClientResponse[0].clientId; + + const getClientSercretResponse = await this.commonService.httpGet( + await this.keycloakUrlService.GetClientSecretURL(realmName, id), + this.getAuthHeader(token) + ); + this.logger.debug( + `ClientRegistrationService get realm admin client secret ${JSON.stringify( + getClientSercretResponse + )}` + ); + this.logger.log(`${getClientSercretResponse.value}`); + const client_secret = getClientSercretResponse.value; + + return { + // response: JSON.stringify( + // registerAppResponse + // ) + clientId: client_id, + clientSecret: client_secret + }; + } + + async registerApplication( + name: string, + organizationId: number, + token: string + ) { + const payload = { + is_token_endpoint_ip_header_trusted: false, + name, + is_first_party: true, + oidc_conformant: true, + sso_disabled: false, + cross_origin_auth: false, + refresh_token: { + rotation_type: 'non-rotating', + expiration_type: 'non-expiring' + }, + jwt_configuration: { + alg: 'RS256', + lifetime_in_seconds: 36000, + secret_encoded: false + }, + app_type: 'non_interactive', + grant_types: ['client_credentials'], + custom_login_page_on: true, + client_metadata: { + organizationId: organizationId.toString() + } + }; + const registerAppResponse = await this.commonService.httpPost( + `${process.env.KEYCLOAK_DOMAIN}${CommonConstants.URL_KEYCLOAK_MANAGEMENT_APPLICATIONS}`, + payload, + this.getAuthHeader(token) + ); + this.logger.debug( + `ClientRegistrationService register app ${JSON.stringify( + registerAppResponse + )}` + ); + + return { + clientId: registerAppResponse.data.client_id, + clientSecret: registerAppResponse.data.client_secret + }; + } + + async authorizeApi(clientId: string, scope: string[], token: string) { + try { + const existingGrantsResponse = await this.commonService.httpGet( + `${process.env.KEYCLOAK_DOMAIN}${CommonConstants.URL_KEYCLOAK_MANAGEMENT_GRANTS}`, + this.getAuthHeader(token) + ); + + // If an grant matching the client id is already found, don't recreate it. + let grantResponse = { data: undefined }; + grantResponse.data = existingGrantsResponse.data.find( + (grant) => grant.client_id === clientId + ); + this.logger.debug( + `ClientRegistrationService existing grant ${JSON.stringify( + grantResponse + )}` + ); + + // Grant wasn't found, so we need to create it + if (!grantResponse.data) { + const payload = { + client_id: clientId, + audience: process.env.AUTH0_AUDIENCE, + scope + }; + grantResponse = await this.commonService.httpPost( + `${process.env.KEYCLOAK_DOMAIN}${CommonConstants.URL_KEYCLOAK_MANAGEMENT_GRANTS}`, + payload, + this.getAuthHeader(token) + ); + this.logger.debug( + `ClientRegistrationService authorize api ${JSON.stringify( + grantResponse + )}` + ); + } + return grantResponse.data.id; + } catch (error) { + throw error; + } + } + + async getToken(payload: ClientCredentialTokenPayloadDto) { + try { + this.logger.log(`getting token : ${JSON.stringify(payload)}`); + if ( + 'client_credentials' !== payload.grant_type || + !payload.client_id || + !payload.client_secret + ) { + 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' + } + }; + const tokenResponse = await this.commonService.httpPost( + await this.keycloakUrlService.GetSATURL('credebl-platform'), + qs.stringify(payload) + , config); + + this.logger.debug( + `ClientRegistrationService token ${JSON.stringify(tokenResponse)}` + ); + return tokenResponse; + } catch (error) { + throw error; + } + } + + async CreateConnection(clientId: string, token: string) { + const payload = { + name: 'TestConnection1', + display_name: 'Connectiondisplay', + strategy: 'auth0', + options: { + enabledDatabaseCustomization: true, + import_mode: false, + customScripts: { + login: 'function login(email, password, callback) {\n //this example uses the "pg" library\n //more info here: https://github.com/brianc/node-postgres\n\n const bcrypt = require(\'bcrypt\');\n const postgres = require(\'pg\');\n\n const conString = `postgres://${configuration.pg_user}:${configuration.pg_pass}@${configuration.pg_ip}/${configuration.pg_db}`;\n postgres.connect(conString, function (err, client, done) {\n if (err) return callback(err);\n\t\t\t\n const query = \'SELECT id, email, password FROM public.user WHERE email = $1 or username = $1\';\n client.query(query, [email], function (err, result) {\n // NOTE: always call done() here to close\n // the connection to the database\n done();\n\n if (err || result.rows.length === 0) return callback(err || new WrongUsernameOrPasswordError(email));\n\n const user = result.rows[0];\n\n //if(password === user.password) {\n this.logger.log(email);\n if (password === user.password) return callback(err || new WrongUsernameOrPasswordError(email));\n\n return callback(null, {\n user_id: user.id,\n email: user.email\n });\n });\n \n });\n //});\n}', + create: 'function create(user, callback) {\n // This script should create a user entry in your existing database. It will\n // be executed when a user attempts to sign up, or when a user is created\n // through the Auth0 dashboard or API.\n // When this script has finished executing, the Login script will be\n // executed immediately afterwards, to verify that the user was created\n // successfully.\n //\n // The user object will always contain the following properties:\n // * email: the user\'s email\n // * password: the password entered by the user, in plain text\n // * tenant: the name of this Auth0 account\n // * client_id: the client ID of the application where the user signed up, or\n // API key if created through the API or Auth0 dashboard\n // * connection: the name of this database connection\n //\n // There are three ways this script can finish:\n // 1. A user was successfully created\n // callback(null);\n // 2. This user already exists in your database\n // callback(new ValidationError("user_exists", "my error message"));\n // 3. Something went wrong while trying to reach your database\n // callback(new Error("my error message"));\n\n const msg = \'Please implement the Create script for this database connection \' +\n \'at https://manage.auth0.com/#/connections/database\';\n return callback(new Error(msg));\n}\n', + delete: 'function remove(id, callback) {\n // This script remove a user from your existing database.\n // It is executed whenever a user is deleted from the API or Auth0 dashboard.\n //\n // There are two ways that this script can finish:\n // 1. The user was removed successfully:\n // callback(null);\n // 2. Something went wrong while trying to reach your database:\n // callback(new Error("my error message"));\n\n const msg = \'Please implement the Delete script for this database \' +\n \'connection at https://manage.auth0.com/#/connections/database\';\n return callback(new Error(msg));\n}\n', + verify: 'function verify(email, callback) {\n // This script should mark the current user\'s email address as verified in\n // your database.\n // It is executed whenever a user clicks the verification link sent by email.\n // These emails can be customized at https://manage.auth0.com/#/emails.\n // It is safe to assume that the user\'s email already exists in your database,\n // because verification emails, if enabled, are sent immediately after a\n // successful signup.\n //\n // There are two ways that this script can finish:\n // 1. The user\'s email was verified successfully\n // callback(null, true);\n // 2. Something went wrong while trying to reach your database:\n // callback(new Error("my error message"));\n //\n // If an error is returned, it will be passed to the query string of the page\n // where the user is being redirected to after clicking the verification link.\n // For example, returning `callback(new Error("error"))` and redirecting to\n // https://example.com would redirect to the following URL:\n // https://example.com?email=alice%40example.com&message=error&success=false\n\n const msg = \'Please implement the Verify script for this database connection \' +\n \'at https://manage.auth0.com/#/connections/database\';\n return callback(new Error(msg));\n}\n', + get_user: 'function getByEmail(email, callback) {\n // This script should retrieve a user profile from your existing database,\n // without authenticating the user.\n // It is used to check if a user exists before executing flows that do not\n // require authentication (signup and password reset).\n //\n // There are three ways this script can finish:\n // 1. A user was successfully found. The profile should be in the following\n // format: https://auth0.com/docs/users/normalized/auth0/normalized-user-profile-schema.\n // callback(null, profile);\n // 2. A user was not found\n // callback(null);\n // 3. Something went wrong while trying to reach your database:\n // callback(new Error("my error message"));\n\n const msg = \'Please implement the Get User script for this database connection \' +\n \'at https://manage.auth0.com/#/connections/database\';\n return callback(new Error(msg));\n}\n', + change_password: 'function changePassword(email, newPassword, callback) {\n // This script should change the password stored for the current user in your\n // database. It is executed when the user clicks on the confirmation link\n // after a reset password request.\n // The content and behavior of password confirmation emails can be customized\n // here: https://manage.auth0.com/#/emails\n // The `newPassword` parameter of this function is in plain text. It must be\n // hashed/salted to match whatever is stored in your database.\n //\n // There are three ways that this script can finish:\n // 1. The user\'s password was updated successfully:\n // callback(null, true);\n // 2. The user\'s password was not updated:\n // callback(null, false);\n // 3. Something went wrong while trying to reach your database:\n // callback(new Error("my error message"));\n //\n // If an error is returned, it will be passed to the query string of the page\n // where the user is being redirected to after clicking the confirmation link.\n // For example, returning `callback(new Error("error"))` and redirecting to\n // https://example.com would redirect to the following URL:\n // https://example.com?email=alice%40example.com&message=error&success=false\n\n const msg = \'Please implement the Change Password script for this database \' +\n \'connection at https://manage.auth0.com/#/connections/database\';\n return callback(new Error(msg));\n}\n' + }, + passwordPolicy: 'good', + password_complexity_options: { + min_length: 8 + }, + password_history: { + size: 5, + enable: false + }, + password_no_personal_info: { + enable: false + }, + password_dictionary: { + enable: false, + dictionary: [] + }, + + gateway_authentication: 'object' + }, + enabled_clients: [clientId], + realms: [''], + metadata: {} + + }; + + const clientConnResponse = await this.commonService.httpPost( + `${process.env.KEYCLOAK_DOMAIN}${CommonConstants.URL_KEYCLOAK_MANAGEMENT_CONNECTIONS}`, + payload, + this.getAuthHeader(token) + ); + this.logger.debug( + `ClientRegistrationService create connection app ${JSON.stringify( + clientConnResponse + )}` + ); + + return { + name: clientConnResponse.data.name, + id: clientConnResponse.data.id + }; + } + + + async getUserToken(email: string, password: string) { + try { + const payload = new userTokenPayloadDto(); + payload.client_id = process.env.KEYCLOAK_MANAGEMENT_CLIENT_ID; + payload.client_secret = process.env.KEYCLOAK_MANAGEMENT_CLIENT_SECRET; + payload.username = email; + payload.password = password; + + this.logger.log(`User Token Payload: ${JSON.stringify(payload)}`); + + + if ( + 'password' !== payload.grant_type || + !payload.client_id || + !payload.client_secret || + !payload.username || + !payload.password + + ) { + 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' + } + }; + + const tokenResponse = await this.commonService.httpPost( + await this.keycloakUrlService.GetSATURL('credebl-platform'), + qs.stringify(payload) + , config); + + this.logger.debug( + `ClientRegistrationService token ${JSON.stringify(tokenResponse)}` + ); + return tokenResponse; + + } catch (error) { + + throw error; + } + } + + async getAccessToken(refreshToken: string) { + try { + const payload = new accessTokenPayloadDto(); + payload.grant_type = 'refresh_token'; + payload.client_id = process.env.KEYCLOAK_MANAGEMENT_CLIENT_ID; + 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 || + !payload.client_secret || + !payload.refresh_token + + ) { + 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' + } + }; + console.log('payload::::', payload); + console.log('typeof payload.refresh_token', typeof payload.refresh_token); + const tokenResponse = await this.commonService.httpPost( + await this.keycloakUrlService.GetSATURL('credebl-platform'), + qs.stringify(payload) + , config); + + this.logger.debug( + `ClientRegistrationService token ${JSON.stringify(tokenResponse)}` + ); + return tokenResponse; + + } catch (error) { + + throw error; + } + } + + async getAccessTokenHolder(refreshToken: string) { + try { + const payload = new accessTokenPayloadDto(); + payload.grant_type = 'refresh_token'; + payload.client_id = process.env.KEYCLOAK_MANAGEMENT_ADEYA_CLIENT_ID; + payload.refresh_token = refreshToken; + payload.client_secret = process.env.KEYCLOAK_MANAGEMENT_ADEYA_CLIENT_SECRET; + + + this.logger.log(`access Token for holderPayload: ${JSON.stringify(payload)}`); + + + if ( + 'refresh_token' !== payload.grant_type || + !payload.client_id || + !payload.client_secret || + !payload.refresh_token + + ) { + throw new Error('Bad Request'); + } + + const strURL = await this.keycloakUrlService.GetSATURL('credebl-platform'); + this.logger.log(`getToken URL: ${strURL}`); + const config = { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + } + }; + + const tokenResponse = await this.commonService.httpPost( + await this.keycloakUrlService.GetSATURL('credebl-platform'), + qs.stringify(payload) + , config); + + this.logger.debug( + `ClientRegistrationService token ${JSON.stringify(tokenResponse)}` + ); + return tokenResponse; + + } catch (error) { + + throw error; + } + } + + +} \ No newline at end of file diff --git a/libs/client-registration/src/dtos/accessTokenPayloadDto.ts b/libs/client-registration/src/dtos/accessTokenPayloadDto.ts new file mode 100644 index 000000000..18294a0bb --- /dev/null +++ b/libs/client-registration/src/dtos/accessTokenPayloadDto.ts @@ -0,0 +1,7 @@ +export class accessTokenPayloadDto { + client_id: string; + client_secret: string; + grant_type?: string = 'refresh_token'; + refresh_token: string; + } + \ No newline at end of file diff --git a/libs/client-registration/src/dtos/client-credential-token-payload.dto.ts b/libs/client-registration/src/dtos/client-credential-token-payload.dto.ts new file mode 100644 index 000000000..6e9c87de8 --- /dev/null +++ b/libs/client-registration/src/dtos/client-credential-token-payload.dto.ts @@ -0,0 +1,7 @@ +export class ClientCredentialTokenPayloadDto { + client_id: string; + client_secret: string; + audience?: string; + grant_type?: string = 'client_credentials'; + scope?: string; +} diff --git a/libs/client-registration/src/dtos/create-user.dto.ts b/libs/client-registration/src/dtos/create-user.dto.ts new file mode 100644 index 000000000..4970d97d1 --- /dev/null +++ b/libs/client-registration/src/dtos/create-user.dto.ts @@ -0,0 +1,23 @@ +/* eslint-disable camelcase */ +import { ApiExtraModels } from '@nestjs/swagger'; +// import { Role } from 'apps/platform-service/src/entities/role.entity'; + +@ApiExtraModels() +export class CreateUserDto { + id?: number; + username?: string; + email: string; + password: string; + logo_uri?: string; + token_lifetime?: number; + is_active?: boolean; + firstName?: string; + lastName?: string; + // role?: Role; + isEmailVerified?: boolean; + createdBy?: number; + clientId?: string; + clientSecret?: string; + keycloakUserId?: string; + +} diff --git a/libs/client-registration/src/dtos/userTokenPayloadDto.ts b/libs/client-registration/src/dtos/userTokenPayloadDto.ts new file mode 100644 index 000000000..d1e89bf86 --- /dev/null +++ b/libs/client-registration/src/dtos/userTokenPayloadDto.ts @@ -0,0 +1,9 @@ +export class userTokenPayloadDto { + client_id: string; + client_secret: string; + username: string; + password: string; + grant_type?: string = 'password'; + + } + \ No newline at end of file diff --git a/libs/client-registration/src/index.ts b/libs/client-registration/src/index.ts new file mode 100644 index 000000000..470b28a05 --- /dev/null +++ b/libs/client-registration/src/index.ts @@ -0,0 +1,2 @@ +export * from './client-registration.module'; +export * from './client-registration.service'; diff --git a/libs/client-registration/tsconfig.lib.json b/libs/client-registration/tsconfig.lib.json new file mode 100644 index 000000000..fb3f93c0f --- /dev/null +++ b/libs/client-registration/tsconfig.lib.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "declaration": true, + "outDir": "../../dist/libs/client-registration" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] +} diff --git a/libs/common/src/cast.helper.ts b/libs/common/src/cast.helper.ts new file mode 100644 index 000000000..5ade7f52f --- /dev/null +++ b/libs/common/src/cast.helper.ts @@ -0,0 +1,47 @@ +interface ToNumberOptions { + default?: number; + min?: number; + max?: number; +} + +export function toLowerCase(value: string): string { + return value.toLowerCase(); +} + +export function trim(value: string): string { + return value.trim(); +} + +export function toDate(value: string): Date { + return new Date(value); +} + +export function toBoolean(value: string): boolean { + // eslint-disable-next-line no-param-reassign + value = value.toLowerCase(); + + // return 'true' === value || '1' === value ? true : false; + + return Boolean('true' === value || '1' === value); + +} + +export function toNumber(value: string, opts: ToNumberOptions = {}): number { + let newValue: number = Number.parseInt(value || String(opts.default), 10); + + if (Number.isNaN(newValue)) { + newValue = opts.default; + } + + if (opts.min) { + if (newValue < opts.min) { + newValue = opts.min; + } + + if (newValue > opts.max) { + newValue = opts.max; + } + } + + return newValue; +} \ No newline at end of file diff --git a/libs/common/src/common.constant.ts b/libs/common/src/common.constant.ts new file mode 100644 index 000000000..7a65d2060 --- /dev/null +++ b/libs/common/src/common.constant.ts @@ -0,0 +1,326 @@ + +export enum CommonConstants { + // Error and Success Responses from POST and GET calls + RESP_ERR_HTTP_INVALID_HEADER_VALUE = 'ERR_HTTP_INVALID_HEADER_VALUE', + RESP_ERR_401 = 401, + RESP_ERR_NOT_FOUND = 404, + RESP_BAD_REQUEST = 400, + RESP_ERR_UNPROCESSABLE_ENTITY = 422, + RESP_SUCCESS_200 = 200, + RESP_SUCCESS_201 = 201, + RESP_SUCCESS_204 = 204, + RESP_ERR_500 = 500, + UNAUTH_MSG = 'UNAUTHORISED ACCESS', + DATA_ALREADY_PRESENT = 'RECORD ALREADY EXIST', + RESP_CONFLICT = 409, + // URL constants for various GET/POST calls + // CONNECTION SERVICES + URL_CONN_GET_CONNECTIONS = '/connections', + URL_CONN_GET_CONNECTION_BY_ID = '/connections/#', + URL_CONN_CREATE_CONNECTION_INVITE = '/connections/create-invitation', + URL_CONN_RECEIVE_CONNECTION_INVITE = '/connections/receive-invitation', + URL_CONN_ACCEPT_CONNECTION_INVITE = '/connections/#/accept-invitation', + URL_CONN_ACCEPT_CONNECTION_REQUEST = '/connections/#/accept-request', + URL_CONN_REMOVE_CONNECTION_BY_ID = '/connections/#/remove', + URL_CONN_METADATA = '/connections/#/metadata', + URL_CONN_LEGACY_INVITE = '/oob/create-legacy-invitation', + + // WALLET SERVICES + URL_WALLET_CREATE_DID = '/wallet/did/create', + URL_WALLET_LIST_DID = '/wallet/did', + URL_WALLET_FETCH_CURR_PUB_DID = '/wallet/did/public', + URL_WALLET_ASSIGN_CURR_DID_PUB = '/wallet/did/public', + URL_WALLET_GET_TAGGING_POLICY = '/wallet/tag-policy/#', + URL_WALLET_SET_TAGGING_POLICY = '/wallet/tag-policy/#', + URL_WALLET_PROVISION = '/wallet/provision', + + // LEDGER SERVICES + URL_LEDG_GET_DID_VERKEY = '/ledger/did-verkey?did=#', + URL_LEDG_REGISTER_NYM = '/ledger/register-nym?did=#&verkey=@&role=$', + URL_LEDG_GET_DID_ENDPOINT = '/ledger/did-endpoint?did=#', + URL_LEDG_GET_TAA = '/ledger/taa', + URL_LEDG_POST_TAA_ACCEPT = '/ledger/taa/accept', + + + // MESSAGING SERVICES + URL_MSG_SEND_MESSAGE = '/connections/#/send-message', + URL_MSG_TRUST_PING = '/connections/#/send-ping', + URL_MSG_BY_CONN = '/basic-message/#', + + // CREDENTIAL ISSUANCE SERVICES + URL_ISSUE_GET_CREDS = '/credentials', + URL_ISSUE_GET_CREDEX_RECS = '/issue-credential/records', + URL_ISSUE_GET_CRED_REC_BY_ID = '/credentials/#', + URL_ISSUE_SEND_CRED = '/credentials/offer-credential', + URL_ISSUE_SEND_CRED_OFFER = '/credentials/offer-credential', + URL_ISSUE_CREATE_SEND_PROPOSAL = '/issue-credential/send-proposal', + URL_ISSUE_CREATE_CRED_OFFER = '/issue-credential/send-offer', + URL_ISSUE_CREAT_CRED_OFFER_BY_CRED_ID = '/credentials/#/accept-proposal', + URL_ISSUE_CREATE_CRED_REQUEST = '/issue-credential/records/#/send-request', + URL_ISSUE_SEND_ISSUED_CRED = '/issue-credential/records/#/issue', + URL_ISSUE_STORE_CRED = '/issue-credential/records/#/store', + URL_ISSUE_REPORT_PROB_CREDEX = '/issue-credential/records/#/problem-report', + URL_ISSUE_REMOVE_CRED = '/issue-credential/records/#/remove', + URL_ISSUE_REVOKE_CRED = '/revocation/revoke', + URL_PUBLISH_REVOCATION = '/issue-credential/publish-revocations', + URL_CREATE_ISSUE_CREDENTIAL_OUT_OF_BAND = '/issue-credential/create', + URL_CREATE_OUT_OF_BAND_INVITATION = '/out-of-band/create-invitation', + URL_ISSUE_CREATE_CRED_OFFER_AFJ= '/credentials/create-offer', + URL_ISSUE_GET_CREDS_AFJ= '/credentials', + URL_ISSUE_GET_CREDS_AFJ_BY_CRED_REC_ID= '/credentials', + + // SCHEMA & CRED DEF SERVICES + URL_SCHM_CREATE_SCHEMA = '/schemas', + URL_SCHM_GET_SCHEMA_BY_ID = '/schemas/#', + URL_SCHM_GET_SCHEMA_BY_ATTRB = '/schemas/created', + URL_SCHM_CREATE_CRED_DEF = '/credential-definitions', + URL_SCHM_GET_CRED_DEF_BY_ID = '/credential-definitions/#', + URL_SCHM_GET_CRED_DEF_BY_ATTRB = '/credential-definitions/created', + + // SHARED AGENT + URL_SHAGENT_CREATE_TENANT = '/multi-tenancy/create-tenant', + URL_SHAGENT_WITH_TENANT_AGENT = '/multi-tenancy/with-tenant-agent', + URL_SHAGENT_CREATE_INVITATION = '/multi-tenancy/create-invitation/#', + URL_SHAGENT_CREATE_OFFER = '/multi-tenancy/credentials/create-offer/#', + URL_SHAGENT_CREATE_OFFER_OUT_OF_BAND = '/multi-tenancy/credentials/create-offer-oob/#', + URL_SHAGENT_GET_CREDENTIALS = '/multi-tenancy/credentials/#', + URL_SHAGENT_GET_CREDENTIALS_BY_CREDENTIAL_ID = '/multi-tenancy/credentials/#/@', + URL_SHAGENT_GET_PROOFS = '/multi-tenancy/proofs/#', + URL_SHAGENT_GET_PROOFS_BY_PRESENTATION_ID = '/multi-tenancy/proofs/#/@', + URL_SHAGENT_REQUEST_PROOF = '/multi-tenancy/proofs/request-proof/#', + URL_SHAGENT_ACCEPT_PRESENTATION = '/multi-tenancy/proofs/@/accept-presentation/#', + + // PROOF SERVICES + URL_SEND_PROOF_REQUEST = '/proofs/request-proof', + URL_GET_PROOF_PRESENTATIONS = '/proofs', + URL_GET_PROOF_PRESENTATION_BY_ID = '/proofs/#', + URL_VERIFY_PRESENTATION = '/proofs/#/accept-presentation', + + // 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_ENDPOINT = '/agent', + + // ENTITY NAMES + ENTITY_NAME_TEMPLATE = 'templates', + ENTITY_NAME_CRED_DEF = 'credential_definition', + ENTITY_NAME_ISSUED_CRED = 'issue_credentials', + ENTITY_NAME_PROOF_REQ = 'proof_request', + ENTITY_NAME_PROOF_PRESENTED = 'presented_proof', + + // ENTITY ACTION + ENTITY_ACTION_INSERT = 'insert', + ENTITY_ACTION_UPDATE = 'update', + ENTITY_ACTION_DELETE = 'delete', + + + // EVENTS + EVENT_AUDIT = 'audit_event', + + // DOMAIN EVENTS + DOMAIN_EVENT_SCHEMA_CREATED = 'Schema Created', + DOMAIN_EVENT_CRED_DEF_CREATED = 'Cred-Def Created', + DOMAIN_EVENT_CRED_ISSUED = 'Credential Issued', + DOMAIN_EVENT_PROOF_REQ = 'Proof Requested', + DOMAIN_EVENT_PROOF_VERIFIED = 'Proof Verified', + DOMAIN_EVENT_CONN_SEND = 'Connection Send', + DOMAIN_EVENT_USER_ONBOARD = 'User Onboard', + DOMAIN_EVENT_WALLET_CREATED = 'Wallet Created', + + // (Platform) admin permissions + PERMISSION_TENANT_MGMT = 'Tenant Management', + PERMISSION_ROLE_MGMT = 'Role Management', + PERMISSION_ORG_REPORTS = 'Organization Reports', + PERMISSION_TENANT_REPORTS = 'Tenant Reports', + + // Tenant permissions + PERMISSION_ORG_MGMT = 'Organization Management', + PERMISSION_MODIFY_ORG = 'Modify Organizations', + + + // Roles And Permissions + PERMISSION_PLATFORM_MANAGEMENT = 'Platform Management', + PERMISSION_USER_MANAGEMENT = 'User Management', + PERMISSION_ROLE_MANAGEMENT = 'Role Management', + + PERMISSION_CONNECTIONS = 'Connections', + + PERMISSION_CREATE_SCHEMA = 'Create Schema', + PERMISSION_VIEW_SCHEMA = 'View Schema', + + PERMISSION_CREATE_CRED_DEF = 'Create Credential Definition', + PERMISSION_VIEW_CRED_DEF = 'View Credential Definition', + + PERMISSION_ISSUE_CREDENTIAL = 'Issue Credential', + + PERMISSION_REVOKE_CREDENTIAL = 'Revoke Credential', + + PERMISSION_SEND_PROOF_REQUEST = 'Send Proof Request', + + PERMISSION_VERIFY_PROOF = 'Verify Proof', + + GENERATE_PRESENTATION_PROOF_REQUEST = '/present-proof/create-request', + + ROLE_TRUST_ANCHOR = 'TRUST_ANCHOR', + ROLE_ENDORSER = 'ENDORSER', + + CONNECTION = 'Connection', + SCHEMA = 'Schema', + CREDENTIAL_DEFINITION = 'Credential Definition', + ISSUE_CREDENTIAL = 'Issue Credentials', + REVOKE = 'Revoke', + PROOF_REQUEST = 'Proof Request', + VERIFY = 'Verify', + + //REVOCATION + URL_REVOC_REG_CREATE = '/revocation/create-registry', + URL_GET_REVOC_REG_DATA = '/revocation/registry/#/tails-file', + URL_UPDATE_FILE = '/revocation/registry/#', + URL_REVOC_PUBLISH = '/revocation/registry/#/publish', + URL_REVOC_GETBY_CREDDEF = '/revocation/active-registry/#', + URL_REVOC_REG_BYID = '/revocation/registry/#', + + // SUBSCRIPTION TYPES + SUBSCRIPTION_COMMON = 'common', + SUBSCRIPTION_BOTH = 'both', + SUBSCRIPTION_ISSUER = 'Issuer', + SUBSCRIPTION_VERIFIER = 'Verifier', + + URL_KEYCLOAK_MANAGEMENT_AUDIENCE = '/api/v2/', + URL_KEYCLOAK_MANAGEMENT_APPLICATIONS = '/api/v2/clients', + URL_KEYCLOAK_MANAGEMENT_APPLICATIONS_SEARCH = '/api/v2/clients/{id}', + URL_KEYCLOAK_MANAGEMENT_GRANTS = '/api/v2/client-grants', + URL_KEYCLOAK_MANAGEMENT_ROLES = '/api/v2/roles', + URL_KEYCLOAK_MANAGEMENT_PERMISSIONS = '/api/v2/roles/{id}/permissions', + URL_KEYCLOAK_AUTHORIZE = '/authorize', + URL_KEYCLOAK_TOKEN = '/oauth/token', + URL_KEYCLOAK_USERINFO = '/userinfo', + URL_KEYCLOAK_CLIENT_SECRET = 'admin/realms/credebl-platform/clients/{id}/client-secret', + URL_KEYCLOAK_JWKS = '/protocol/openid-connect/certs', + URL_KEYCLOAK_MANAGEMENT_CONNECTIONS = '/api/v2/connections', + SET_TRANSACTION_ROLE = '/transactions/#/set-endorser-role', + SET_TRANSACTION_INFO = '/transactions/#/set-endorser-info', + TRANSACTION_CREATE_REQUEST = '/transactions/create-request', + ENDORSE_TRANSACTION = '/transactions/#/endorse', + WRITE_TRANSACTION = '/transactions/#/write', + + // Tenant Status + PENDING_STATE = 0, + REJECT_STATE = 2, + APPROVE_STATE = 1, + + //User roles + TENANT_ROLE = 2, + SUPER_ADMIN_ROLE = 4, + PLATFORM_ADMIN_ROLE = 1, + ORG_ROLE = 3, + + ORG_PLATFORM_ROLE = 1, + ORG_TENANT_ROLE = 2, + ORG_ENTITY_ROLE = 3, + + // Organizations Status + PENDING_ORG = 0, + REJECT_ORG = 2, + APPROVE_ORG = 1, + + // Organizations Status + PENDING_NON_ADMIN_USER = 0, + INACTIVE_NON_ADMIN_USER = 2, + 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', + + + //onBoarding Type + ONBOARDING_TYPE_ADMIN = 0, + ONBOARDING_TYPE_EXTERNAL = 1, + ONBOARDING_TYPE_INVITATION = 2, + + + // Network + TESTNET = 'testnet', + STAGINGNET = 'stagingnet', + BUILDERNET = 'buildernet', + MAINNET = 'mainnet', + LIVENET = 'livenet', + + + // Features Id + SCHEMA_CREATION = 1, + CREATE_CREDENTIAL_DEFINITION = 2, + CREATION_OF_ATTRIBUTE = 3, + CREDENTIAL_ISSUANCE = 4, + REVOCATION_REGISTRY = 5, + REVOCATION_UPDATE = 6, + VERIFY_PROOF = 7, + ENDORSER_DID = 8, + ORGANIZATION_CREATION = 9, + ADD_USER = 10, +} + +export const postgresqlErrorCodes = []; +postgresqlErrorCodes['23503'] = 'foreign_key_violation'; +postgresqlErrorCodes['00000'] = 'successful_completion'; +postgresqlErrorCodes['01000'] = 'warning'; +postgresqlErrorCodes['0100C'] = 'dynamic_result_sets_returned'; +postgresqlErrorCodes['01008'] = 'implicit_zero_bit_padding'; +postgresqlErrorCodes['01003'] = 'null_value_eliminated_in_set_function'; +postgresqlErrorCodes['01007'] = 'privilege_not_granted'; +postgresqlErrorCodes['01006'] = 'string_data_right_truncation'; +postgresqlErrorCodes['01P01'] = 'deprecated_feature'; +postgresqlErrorCodes['02000'] = 'no_data'; + +postgresqlErrorCodes['02001'] = 'no_additional_dynamic_result_sets_returned'; +postgresqlErrorCodes['03000'] = 'sql_statement_not_yet_complete'; +postgresqlErrorCodes['08000'] = 'connection_exception'; +postgresqlErrorCodes['08003'] = 'connection_does_not_exist'; +postgresqlErrorCodes['08006'] = 'connection_failure'; +postgresqlErrorCodes['08001'] = 'sqlclient_unable_to_establish_sqlconnection'; +postgresqlErrorCodes['08004'] = 'sqlserver_rejected_establishment_of_sqlconnection'; +postgresqlErrorCodes['08007'] = 'transaction_resolution_unknown'; +postgresqlErrorCodes['08P01'] = 'protocol_violation'; +postgresqlErrorCodes['09000'] = 'triggered_action_exception'; +postgresqlErrorCodes['0A000'] = 'feature_not_supported'; +postgresqlErrorCodes['0B000'] = 'invalid_transaction_initiation'; +postgresqlErrorCodes['0F000'] = 'locator_exception'; +postgresqlErrorCodes['0F001'] = 'invalid_locator_specification'; +postgresqlErrorCodes['0L000'] = 'invalid_grantor'; +postgresqlErrorCodes['0LP01'] = 'invalid_grant_operation'; +postgresqlErrorCodes['0P000'] = 'invalid_role_specification'; +postgresqlErrorCodes['0Z000'] = 'diagnostics_exception'; +postgresqlErrorCodes['0Z002'] = 'stacked_diagnostics_accessed_without_active_handler'; +postgresqlErrorCodes['20000'] = 'case_not_found'; +postgresqlErrorCodes['21000'] = 'cardinality_violation'; +postgresqlErrorCodes['22000'] = 'data_exception'; +postgresqlErrorCodes['2202E'] = 'array_subscript_error'; +postgresqlErrorCodes['22021'] = 'character_not_in_repertoire'; +postgresqlErrorCodes['22008'] = 'datetime_field_overflow'; +postgresqlErrorCodes['22012'] = 'division_by_zero'; +postgresqlErrorCodes['22005'] = 'error_in_assignment'; +postgresqlErrorCodes['2200B'] = 'escape_character_conflict'; + + +postgresqlErrorCodes['22022'] = 'indicator_overflow'; +postgresqlErrorCodes['22015'] = 'interval_field_overflow'; +postgresqlErrorCodes['2201E'] = 'invalid_argument_for_logarithm'; +postgresqlErrorCodes['22014'] = 'invalid_argument_for_ntile_function'; +postgresqlErrorCodes['22016'] = 'invalid_argument_for_nth_value_function'; +postgresqlErrorCodes['2201F'] = 'invalid_argument_for_power_function'; +postgresqlErrorCodes['2201G'] = 'invalid_argument_for_width_bucket_function'; +postgresqlErrorCodes['22018'] = 'invalid_character_value_for_cast'; +postgresqlErrorCodes['22007'] = 'invalid_datetime_format'; +postgresqlErrorCodes['22019'] = 'invalid_escape_character'; +postgresqlErrorCodes['22P02'] = 'invalid_datatype'; +postgresqlErrorCodes[''] = ''; + diff --git a/libs/common/src/common.module.ts b/libs/common/src/common.module.ts new file mode 100644 index 000000000..2b7967c30 --- /dev/null +++ b/libs/common/src/common.module.ts @@ -0,0 +1,11 @@ +import { HttpModule } from '@nestjs/axios'; +import { Module } from '@nestjs/common'; + +import { CommonService } from './common.service'; + +@Module({ + imports: [HttpModule], + providers: [CommonService], + exports: [CommonService] +}) +export class CommonModule {} diff --git a/libs/common/src/common.service.spec.ts b/libs/common/src/common.service.spec.ts new file mode 100644 index 000000000..385622c1f --- /dev/null +++ b/libs/common/src/common.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { CommonService } from './common.service'; + +describe('CommonService', () => { + let service: CommonService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [CommonService] + }).compile(); + + service = module.get(CommonService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/libs/common/src/common.service.ts b/libs/common/src/common.service.ts new file mode 100644 index 000000000..092086221 --- /dev/null +++ b/libs/common/src/common.service.ts @@ -0,0 +1,328 @@ +import * as CryptoJS from 'crypto-js'; + +import { + BadRequestException, + HttpException, + HttpStatus, + Injectable, + Logger +} from '@nestjs/common'; + +import { CommonConstants } from './common.constant'; +import { HttpService } from '@nestjs/axios/dist'; +import { ResponseService } from '@credebl/response'; + +@Injectable() +export class CommonService { + private readonly logger = new Logger('CommonService'); + result: ResponseService = new ResponseService(); + + constructor(private readonly httpService: HttpService) {} + + 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.log(`SUCCESS in POST : ${JSON.stringify(response.data)}`); + this.logger.error(response.data); + return response.data; + }); + } catch (error) { + this.logger.error(`ERROR in POST : ${error}`); + if ( + error + .toString() + .includes(CommonConstants.RESP_ERR_HTTP_INVALID_HEADER_VALUE) + ) { + throw new HttpException( + { + statusCode: HttpStatus.UNAUTHORIZED, + error: CommonConstants.UNAUTH_MSG + }, + HttpStatus.UNAUTHORIZED + ); + } + if (error.toString().includes(CommonConstants.RESP_ERR_NOT_FOUND)) { + throw new HttpException( + { + statusCode: HttpStatus.NOT_FOUND, + error: error.message + }, + HttpStatus.NOT_FOUND + ); + } + if (error.toString().includes(CommonConstants.RESP_BAD_REQUEST)) { + throw new HttpException( + { + statusCode: HttpStatus.BAD_REQUEST, + error: error.message + }, + HttpStatus.BAD_REQUEST + ); + } + if ( + error.toString().includes(CommonConstants.RESP_ERR_UNPROCESSABLE_ENTITY) + ) { + throw new HttpException( + { + statusCode: HttpStatus.UNPROCESSABLE_ENTITY, + error: error.message + }, + HttpStatus.UNPROCESSABLE_ENTITY + ); + } else { + throw new HttpException( + { + statusCode: HttpStatus.INTERNAL_SERVER_ERROR, + error: 'Something went wrong.' + }, + HttpStatus.INTERNAL_SERVER_ERROR + ); + } + } + } + + async httpGet(url: string, config?: any) { + try { + this.logger.debug(`httpGet service URL: ${url}`); + return await this.httpService + .get(url, config) + .toPromise() + .then((data) => + // this.logger.log(`Success Data: ${JSON.stringify(data.data)}`); + data.data + ); + } catch (error) { + this.logger.error(`ERROR in GET : ${JSON.stringify(error)}`); + if ( + error + .toString() + .includes(CommonConstants.RESP_ERR_HTTP_INVALID_HEADER_VALUE) + ) { + throw new HttpException( + { + statusCode: HttpStatus.UNAUTHORIZED, + error: CommonConstants.UNAUTH_MSG + }, + HttpStatus.UNAUTHORIZED + ); + } + if (error.toString().includes(CommonConstants.RESP_ERR_NOT_FOUND)) { + throw new HttpException( + { + statusCode: HttpStatus.NOT_FOUND, + error: error.message + }, + HttpStatus.NOT_FOUND + ); + } + if (error.toString().includes(CommonConstants.RESP_BAD_REQUEST)) { + throw new HttpException( + { + statusCode: HttpStatus.BAD_REQUEST, + error: error.message + }, + HttpStatus.BAD_REQUEST + ); + } + if ( + error.toString().includes(CommonConstants.RESP_ERR_UNPROCESSABLE_ENTITY) + ) { + throw new HttpException( + { + statusCode: HttpStatus.UNPROCESSABLE_ENTITY, + error: error.message + }, + HttpStatus.UNPROCESSABLE_ENTITY + ); + } else { + throw new HttpException( + { + statusCode: HttpStatus.INTERNAL_SERVER_ERROR, + error: 'Something went wrong.' + }, + HttpStatus.INTERNAL_SERVER_ERROR + ); + } + } + } + + 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() + .then((response: any) => { + this.logger.log(`SUCCESS in POST : ${JSON.stringify(response.data)}`); + return response.data; + }); + } catch (error) { + this.logger.error(`ERROR in PATCH : ${JSON.stringify(error)}`); + if ( + error + .toString() + .includes(CommonConstants.RESP_ERR_HTTP_INVALID_HEADER_VALUE) + ) { + throw new HttpException( + { + statusCode: HttpStatus.UNAUTHORIZED, + error: CommonConstants.UNAUTH_MSG + }, + HttpStatus.UNAUTHORIZED + ); + } + if (error.toString().includes(CommonConstants.RESP_ERR_NOT_FOUND)) { + throw new HttpException( + { + statusCode: HttpStatus.NOT_FOUND, + error: error.message + }, + HttpStatus.NOT_FOUND + ); + } + if (error.toString().includes(CommonConstants.RESP_BAD_REQUEST)) { + throw new HttpException( + { + statusCode: HttpStatus.BAD_REQUEST, + error: error.message + }, + HttpStatus.BAD_REQUEST + ); + } + if ( + error.toString().includes(CommonConstants.RESP_ERR_UNPROCESSABLE_ENTITY) + ) { + throw new HttpException( + { + statusCode: HttpStatus.UNPROCESSABLE_ENTITY, + error: error.message + }, + HttpStatus.UNPROCESSABLE_ENTITY + ); + } else { + throw new HttpException( + { + statusCode: HttpStatus.INTERNAL_SERVER_ERROR, + error: 'Something went wrong.' + }, + HttpStatus.INTERNAL_SERVER_ERROR + ); + } + } + } + + async httpPut( + url: string, + payload?: any, + 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(); + + return this.filterResponse(response); + } catch (error) { + return this.sendError(error); + } + } + + filterResponse(data: any) { + let response; + if ( + data.data && + data.data.message !== undefined && + data.data.success !== undefined + ) { + this.logger.debug( + `CommonService: data is already a response object, return` + ); + response = data.data; + } else { + this.logger.debug( + `CommonService: create response object: ${JSON.stringify(data?.data)}` + ); + response = this.result.response( + 'fetched', + true, + !data.data.results + ? !data.data.result + ? data.data + : data.data.result + : data.data + ); + } + + return response; + } + + sendError(error: any): ResponseService { + this.logger.error( + `in sendError: ${error} StatusCode: ${error.response?.status}` + ); + if (error.response?.status) { + throw new HttpException( + { + statusCode: error.response.status, + error: error.message + }, + error.response.status + ); + } else { + throw new HttpException( + { + statusCode: HttpStatus.INTERNAL_SERVER_ERROR, + error: error.message + }, + HttpStatus.INTERNAL_SERVER_ERROR + ); + } + } + + // To validate space in string + spaceValidate(text, customMessage) { + if ('' === text.toString().trim()) { + throw new BadRequestException(customMessage); + } + } + // To validate password + passwordValidation(password) { + const passwordRegEx = /^(?=.*[0-9])(?=.*[A-Z])(?=.*[a-z])(?=.*[-!@$%^*])(?=.*[!"$%*,-.\/:;=@^_])[a-zA-Z0-9!"$%*,-.\/:;=@^_]{8,}$/; + const defaultMessage = + 'Passwords must contain at least 8 characters, including uppercase, lowercase, numbers and special character.'; + if (!passwordRegEx.test(password.trim())) { + throw new BadRequestException(defaultMessage); + } + } + // To decrypt password + decryptPassword(encryptedPassword) { + try { + const password = CryptoJS.AES.decrypt( + 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/common.utils.ts b/libs/common/src/common.utils.ts new file mode 100644 index 000000000..15aa8d89e --- /dev/null +++ b/libs/common/src/common.utils.ts @@ -0,0 +1,41 @@ +export function paginator( + items: T[], + current_page: number, + items_per_page: number +) { + const page = current_page || 1, + per_page = items_per_page || 10, + offset = (page - 1) * per_page, + paginatedItems = items.slice(offset).slice(0, items_per_page), + total_pages = Math.ceil(items.length / per_page); + + return { + page, + items_per_page: per_page, + previousPage: page - 1 ? page - 1 : null, + nextPage: total_pages > page ? page + 1 : null, + totalItems: items.length, + lastPage: total_pages, + data: paginatedItems + }; +} + +export function orderValues(key, order = 'asc') { + return function innerSort(a, b) { + if (!a.hasOwnProperty(key) || !b.hasOwnProperty(key)) { + // property doesn't exist on either object + return 0; + } + + const varA = 'string' === typeof a[key] ? a[key].toUpperCase() : a[key]; + const varB = 'string' === typeof b[key] ? b[key].toUpperCase() : b[key]; + + let comparison = 0; + if (varA > varB) { + comparison = 1; + } else if (varA < varB) { + comparison = -1; + } + return 'desc' === order ? comparison * -1 : comparison; + }; +} diff --git a/libs/common/src/dtos/email.dto.ts b/libs/common/src/dtos/email.dto.ts new file mode 100644 index 000000000..a810bed10 --- /dev/null +++ b/libs/common/src/dtos/email.dto.ts @@ -0,0 +1,7 @@ +export class EmailDto { + emailFrom: string; + emailTo: string; + emailSubject: string; + emailText: string; + emailHtml: string; +} diff --git a/libs/common/src/exception-handler.ts b/libs/common/src/exception-handler.ts new file mode 100644 index 000000000..554f1dfff --- /dev/null +++ b/libs/common/src/exception-handler.ts @@ -0,0 +1,69 @@ +import { + ExceptionFilter, + Catch, + ArgumentsHost, + HttpException, + HttpStatus, + RpcExceptionFilter +} from '@nestjs/common'; +import { HttpAdapterHost } from '@nestjs/core'; +import { RpcException } from '@nestjs/microservices'; +import { Observable, throwError } from 'rxjs'; + +@Catch() +export class AllExceptionsFilter implements ExceptionFilter { + constructor(private readonly httpAdapterHost: HttpAdapterHost) { } + + // Add explicit types for 'exception' and 'host' + catch(exception: any, host: ArgumentsHost): void { + + const { httpAdapter } = this.httpAdapterHost; + const ctx = host.switchToHttp(); + const request = ctx.getRequest(); + + let httpStatus = exception.status; //HttpStatus.INTERNAL_SERVER_ERROR; + let message = ''; + + switch (exception.constructor) { + case HttpException: + + httpStatus = (exception as HttpException).getStatus(); + + message = exception?.response?.error || exception?.message || 'Internal server error'; + break; + case RpcException: + httpStatus = exception?.code || exception?.error?.code || HttpStatus.BAD_REQUEST; + message = exception?.response.error; + break; + default: + httpStatus = + exception.response?.status || + exception.response?.statusCode || + exception.code || + HttpStatus.INTERNAL_SERVER_ERROR; + message = + exception.response?.data?.message || + exception.response?.message || + exception?.message || + 'Internal server error'; + + } + + const responseBody = { + statusCode: httpStatus, + message, + error: exception.message + }; + + const data = httpAdapter.reply(ctx.getResponse(), responseBody, httpStatus); + + } +} + +@Catch(RpcException) +export class CustomExceptionFilter implements RpcExceptionFilter { + // Add explicit types for 'exception' and 'host' + catch(exception: RpcException, host: ArgumentsHost): Observable { + return throwError(() => new RpcException({ message: exception.getError(), code: HttpStatus.BAD_REQUEST })); + } +} diff --git a/libs/common/src/index.ts b/libs/common/src/index.ts new file mode 100644 index 000000000..10e9ae0ae --- /dev/null +++ b/libs/common/src/index.ts @@ -0,0 +1,2 @@ +export * from './common.module'; +export * from './common.service'; diff --git a/libs/common/src/interfaces/interface.ts b/libs/common/src/interfaces/interface.ts new file mode 100644 index 000000000..fa446c6df --- /dev/null +++ b/libs/common/src/interfaces/interface.ts @@ -0,0 +1,7 @@ +export interface ResponseType { + statusCode: number; + message: string; + data?: Record | string; + error?: Record | string; + } + \ No newline at end of file diff --git a/libs/common/src/interfaces/response.interface.ts b/libs/common/src/interfaces/response.interface.ts new file mode 100644 index 000000000..7a7211741 --- /dev/null +++ b/libs/common/src/interfaces/response.interface.ts @@ -0,0 +1,6 @@ +export default interface IResponseType { + statusCode: number; + message?: string; + data?: unknown; + error?: unknown; +}; diff --git a/libs/common/src/response-messages/index.ts b/libs/common/src/response-messages/index.ts new file mode 100644 index 000000000..f84a9a28c --- /dev/null +++ b/libs/common/src/response-messages/index.ts @@ -0,0 +1,159 @@ + +export const ResponseMessages = { + user: { + success: { + create: 'User registered successfully', + emaiVerified: 'Email verified successfully', + login: 'User login successfully', + fetchProfile: 'User fetched successfully', + fetchInvitations: 'Org invitations fetched successfully', + invitationReject: 'Organization invitation rejected', + invitationAccept: 'Organization invitation accepted', + fetchUsers: 'Users fetched successfully', + newUser: 'User not found', + checkEmail: 'User email checked successfully.', + sendVerificationCode: 'Verification code has been sent sucessfully to the mail. Please verify' + }, + error: { + exists: 'User already exists', + verificationAlreadySent: 'The verification link has already been sent to your email address', + emailSend: 'Unable to send email to the user', + invalidEmailUrl: 'Invalid token or EmailId!', + verifiedEmail: 'Email already verified', + notFound: 'User not found', + verifyMail: 'Please verify your email', + invalidCredentials: 'Invalid Credentials', + registerFido: 'Please complete your fido registration', + invitationNotFound: 'Invitation not found', + invalidInvitationStatus: 'Invalid invitation status', + invalidKeycloakId: 'keycloakId is invalid', + invalidEmail: 'Invalid Email Id!', + adduser: 'Unable to add user details', + verifyEmail: 'The verification link has already been sent to your email address. please verify' + } + }, + organisation: { + success: { + create: 'Organization created successfully', + update: 'Organization updated successfully', + fetchOrgRoles: 'Organization roles fetched successfully', + createInvitation: 'Organization invitations sent successfully', + getInvitation: 'Organization invitations fetched successfully', + getOrganization: 'Organization details fetched successfully', + getOrgDashboard: 'Organization dashboard details fetched', + getOrganizations: 'Organizations details fetched successfully', + updateUserRoles: 'User roles updated successfully' + }, + error: { + exists: 'An organization name is already exist', + rolesNotExist: 'Provided roles not exists in the platform', + userNotFound: 'User not found for the given organization', + updateUserRoles: 'Unable to update user roles' + } + + }, + + fido: { + success: { + RegistrationOption: 'Registration option created successfully', + verifyRegistration: 'Verify registration sucessfully', + updateUserDetails: 'User details updated successfully', + generateAuthenticationOption: 'Authentication option generated successfully', + deleteDevice: 'Device deleted sucessfully', + updateDeviceName: 'Device name updated sucessfully', + login: 'User login successfully' + }, + error: { + exists: 'User already exists', + verification: 'Fail to verify user', + verificationAlreadySent: 'The verification link has already been sent to your email address', + generateRegistration: 'Unable to generate registration option for user', + verifiedEmail: 'Email already verified', + deviceNotFound: 'Device does not exist or revoked', + updateFidoUser: 'Error in updating fido user.', + invalidCredentials: 'Invalid Credentials', + registerFido: 'Please complete your fido registration' + } + }, + + schema: { + success: { + fetch: 'Schema retrieved successfully.', + create: 'Schema created successfully.' + }, + error: { + invalidSchemaId: 'Invalid schema Id provided.', + invalidVersion: 'Invalid schema version provided.', + insufficientAttributes: 'Please provide at least one attribute.', + invalidAttributes: 'Please provide unique attributes', + emptyData: 'Please provide data for creating schema.', + exists: 'Schema already exists', + notCreated: 'Schema not created', + notFound: 'Schema records not found', + schemaIdNotFound: 'SchemaLedgerId not found', + credentialDefinitionNotFound: 'No credential definition exist' + } + }, + credentialDefinition: { + success: { + fetch: 'Credential definition fetched successfully.', + create: 'Credential definition created successfully.' + }, + error: { + NotFound: 'No credential definitions found.', + NotSaved: 'Error in saving credential definition.', + Conflict: 'Tag already exists', + schemaIdNotFound: 'SchemaLedgerId not found', + OrgDidNotFound: 'OrgDid not found', + credDefIdNotFound: 'Credential Definition Id not found' + } + }, + agent: { + success: { + create: 'Agent spin-up up successfully' + }, + error: { + exists: 'An agent name is already exist', + orgNotFound: 'Organization not found', + apiEndpointNotFound: 'apiEndpoint not found', + notAbleToSpinUpAgent: 'Agent not able to spin-up', + alreadySpinUp: 'Agent already spin-up' + } + }, + connection: { + success: { + create: 'Connection created successfully', + fetch: 'Connection Details fetched successfully' + }, + error: { + exists: 'Connection is already exist', + connectionNotFound: 'ConnectionNotFound not found', + agentEndPointNotFound: 'agentEndPoint Not Found', + agentUrlNotFound: 'agent url not found' + } + }, + issuance: { + success: { + create: 'Issue-credential offer created successfully', + fetch: 'Issue-credential fetched successfully' + + }, + error: { + exists: 'Credentials is already exist', + credentialsNotFound: 'Credentials not found', + agentEndPointNotFound: 'agentEndPoint Not Found', + agentUrlNotFound: 'agent url not found' + } + }, + verification: { + success: { + fetch: 'Proof presentation received successfully.', + create: 'Proof request created successfully.', + verified: 'Proof presentation verified successfully.' + }, + error: { + notFound: 'Organization agent not found', + agentUrlNotFound: 'agent url not found' + } + } +}; diff --git a/libs/common/src/send-grid-helper-file.ts b/libs/common/src/send-grid-helper-file.ts new file mode 100644 index 000000000..d234c6ba3 --- /dev/null +++ b/libs/common/src/send-grid-helper-file.ts @@ -0,0 +1,26 @@ +import * as sendgrid from '@sendgrid/mail'; +import * as dotenv from 'dotenv'; +import { EmailDto } from './dtos/email.dto'; + +dotenv.config(); + +sendgrid.setApiKey( + process.env.SENDGRID_API_KEY +); + +export const sendEmail = async (EmailDto: EmailDto): Promise => { + try { + const msg = { + to: EmailDto.emailTo, + from: EmailDto.emailFrom, + subject: EmailDto.emailSubject, + text: EmailDto.emailText, + html: EmailDto.emailHtml + }; + + return await sendgrid.send(msg).then(() => true).catch(() => false); + } catch (error) { + return false; + } + +}; diff --git a/libs/common/tsconfig.lib.json b/libs/common/tsconfig.lib.json new file mode 100644 index 000000000..8fdbf52b4 --- /dev/null +++ b/libs/common/tsconfig.lib.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "declaration": true, + "outDir": "../../dist/libs/common" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] +} diff --git a/libs/entities/base.number.entity.ts b/libs/entities/base.number.entity.ts new file mode 100644 index 000000000..9bb3b024a --- /dev/null +++ b/libs/entities/base.number.entity.ts @@ -0,0 +1,19 @@ +// base.entity.ts +import { PrimaryGeneratedColumn, Column, UpdateDateColumn, CreateDateColumn, BaseEntity } from 'typeorm'; + +export abstract class AbstractEntity extends BaseEntity { + @PrimaryGeneratedColumn() + id: number; + + @CreateDateColumn({ type: 'timestamptz', default: () => 'CURRENT_TIMESTAMP' }) + createDateTime: Date; + + @Column({ type: 'int', default: 1, nullable: false }) + createdBy: number; + + @UpdateDateColumn({ type: 'timestamptz', default: () => 'CURRENT_TIMESTAMP' }) + lastChangedDateTime: Date; + + @Column({ type: 'int', default: 1, nullable: false }) + lastChangedBy: number; +} \ No newline at end of file diff --git a/libs/entities/base.system.entity.ts b/libs/entities/base.system.entity.ts new file mode 100644 index 000000000..27d65fed4 --- /dev/null +++ b/libs/entities/base.system.entity.ts @@ -0,0 +1,25 @@ +// base.entity.ts +import { + BaseEntity, + Column, + CreateDateColumn, + PrimaryGeneratedColumn, + UpdateDateColumn +} from 'typeorm'; + +export abstract class AbstractEntity extends BaseEntity { + @PrimaryGeneratedColumn() + id: number; + + @CreateDateColumn({ type: 'timestamptz', default: () => 'CURRENT_TIMESTAMP' }) + createDateTime: Date; + + @Column({ type: 'varchar', length: 64, default: 'system' }) + createdBy: string; + + @UpdateDateColumn({ type: 'timestamptz', default: () => 'CURRENT_TIMESTAMP' }) + lastChangedDateTime: Date; + + @Column({ type: 'varchar', length: 64, default: 'system' }) + lastChangedBy: string; +} diff --git a/libs/enum/src/enum.module.ts b/libs/enum/src/enum.module.ts new file mode 100644 index 000000000..3d3e871ff --- /dev/null +++ b/libs/enum/src/enum.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { EnumService } from './enum.service'; + +@Module({ + providers: [EnumService], + exports: [EnumService] +}) +export class EnumModule {} diff --git a/libs/enum/src/enum.service.spec.ts b/libs/enum/src/enum.service.spec.ts new file mode 100644 index 000000000..59f9cbe13 --- /dev/null +++ b/libs/enum/src/enum.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { EnumService } from './enum.service'; + +describe('EnumService', () => { + let service: EnumService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [EnumService] + }).compile(); + + service = module.get(EnumService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/libs/enum/src/enum.service.ts b/libs/enum/src/enum.service.ts new file mode 100644 index 000000000..6b825b134 --- /dev/null +++ b/libs/enum/src/enum.service.ts @@ -0,0 +1,4 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class EnumService {} diff --git a/libs/enum/src/enum.ts b/libs/enum/src/enum.ts new file mode 100644 index 000000000..a59e0c81e --- /dev/null +++ b/libs/enum/src/enum.ts @@ -0,0 +1,19 @@ +export enum SortValue { + ASC = 'ASC', + DESC = 'DESC' +} + +export enum AgentType { + AFJ = 1, + ACAPY = 2 +} +export enum Invitation { + ACCEPTED = 'accepted', + REJECTED = 'rejected', + PENDING = 'pending' +} + +export enum OrgAgentType { + DEDICATED = 1, + SHARED = 2 +} diff --git a/libs/enum/src/index.ts b/libs/enum/src/index.ts new file mode 100644 index 000000000..535c1bd49 --- /dev/null +++ b/libs/enum/src/index.ts @@ -0,0 +1,2 @@ +export * from './enum.module'; +export * from './enum.service'; diff --git a/libs/enum/tsconfig.lib.json b/libs/enum/tsconfig.lib.json new file mode 100644 index 000000000..c388798d3 --- /dev/null +++ b/libs/enum/tsconfig.lib.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "declaration": true, + "outDir": "../../dist/libs/enum" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] +} diff --git a/libs/http-exception.filter.ts b/libs/http-exception.filter.ts new file mode 100644 index 000000000..44656a75b --- /dev/null +++ b/libs/http-exception.filter.ts @@ -0,0 +1,15 @@ +import { Catch, ExceptionFilter, HttpException, Logger } from '@nestjs/common'; +import { RpcException } from '@nestjs/microservices'; + +@Catch(HttpException) +export class HttpExceptionFilter implements ExceptionFilter { + private readonly logger = new Logger('CommonService'); + + catch(exception: HttpException) { + this.logger.log( + `ExceptionFilter caught error: ${JSON.stringify(exception)}` + ); + + throw new RpcException(exception); + } +} diff --git a/libs/keycloak-url/src/index.ts b/libs/keycloak-url/src/index.ts new file mode 100644 index 000000000..4457df5ce --- /dev/null +++ b/libs/keycloak-url/src/index.ts @@ -0,0 +1,2 @@ +export * from './keycloak-url.module'; +export * from './keycloak-url.service'; diff --git a/libs/keycloak-url/src/keycloak-url.module.ts b/libs/keycloak-url/src/keycloak-url.module.ts new file mode 100644 index 000000000..02d0c52ea --- /dev/null +++ b/libs/keycloak-url/src/keycloak-url.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { KeycloakUrlService } from './keycloak-url.service'; + +@Module({ + providers: [KeycloakUrlService], + exports: [KeycloakUrlService] +}) +export class KeycloakUrlModule {} diff --git a/libs/keycloak-url/src/keycloak-url.service.ts b/libs/keycloak-url/src/keycloak-url.service.ts new file mode 100644 index 000000000..28ed838f6 --- /dev/null +++ b/libs/keycloak-url/src/keycloak-url.service.ts @@ -0,0 +1,74 @@ +import { Injectable, Logger } from '@nestjs/common'; + +@Injectable() +export class KeycloakUrlService { + private readonly logger = new Logger('KeycloakUrlService'); + + + async createUserURL( + realm: string + ):Promise { + + return `${process.env.KEYCLOAK_DOMAIN}admin/realms/${realm}/users`; + } + + async getUserByUsernameURL( + realm: string, + username: string + ):Promise { + + return `${process.env.KEYCLOAK_DOMAIN}admin/realms/${realm}/users?username=${username}`; + } + + async GetUserInfoURL( + realm: string, + userid: string + ):Promise { + + return `${process.env.KEYCLOAK_DOMAIN}admin/realms/${realm}/users/${userid}`; + } + + async GetSATURL( + realm: string + ):Promise { + + return `${process.env.KEYCLOAK_DOMAIN}realms/${realm}/protocol/openid-connect/token`; + } + + async ResetPasswordURL( + realm: string, + userid: string + ):Promise { + + return `${process.env.KEYCLOAK_DOMAIN}admin/realms/${realm}/users/${userid}/reset-password`; + } + + + async CreateRealmURL():Promise { + return `${process.env.KEYCLOAK_DOMAIN}admin/realms`; + } + + async createClientURL( + realm: string + ):Promise { + + return `${process.env.KEYCLOAK_DOMAIN}admin/realms/${realm}/clients`; + } + + async GetClientURL( + realm: string, + clientid: string + ):Promise { + + return `${process.env.KEYCLOAK_DOMAIN}admin/realms/${realm}/clients?clientId=${clientid}`; + } + + async GetClientSecretURL( + realm: string, + clientid: string + ):Promise { + + return `${process.env.KEYCLOAK_DOMAIN}admin/realms/${realm}/clients/${clientid}/client-secret`; + } + +} diff --git a/libs/keycloak-url/tsconfig.lib.json b/libs/keycloak-url/tsconfig.lib.json new file mode 100644 index 000000000..6218a7ca6 --- /dev/null +++ b/libs/keycloak-url/tsconfig.lib.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "declaration": true, + "outDir": "../../dist/libs/keycloak-url" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] +} diff --git a/libs/nest-cli.json b/libs/nest-cli.json new file mode 100644 index 000000000..eae42f5bc --- /dev/null +++ b/libs/nest-cli.json @@ -0,0 +1,16 @@ +{ + "projects": { + "keycloak-url": { + "type": "library", + "root": "libs/keycloak-url", + "entryFile": "index", + "sourceRoot": "libs/keycloak-url/src", + "compilerOptions": { + "tsConfigPath": "libs/keycloak-url/tsconfig.lib.json" + } + } + }, + "compilerOptions": { + "webpack": true + } +} \ No newline at end of file diff --git a/libs/org-roles/enums/index.ts b/libs/org-roles/enums/index.ts new file mode 100644 index 000000000..9028205e4 --- /dev/null +++ b/libs/org-roles/enums/index.ts @@ -0,0 +1,9 @@ +export enum OrgRoles { + OWNER = 'owner', + SUPER_ADMIN = 'super_admin', + ADMIN = 'admin', + ISSUER = 'issuer', + VERIFIER = 'verifier', + HOLDER = 'holder', + MEMBER = 'member', +} \ No newline at end of file diff --git a/libs/org-roles/repositories/index.ts b/libs/org-roles/repositories/index.ts new file mode 100644 index 000000000..83372f146 --- /dev/null +++ b/libs/org-roles/repositories/index.ts @@ -0,0 +1,64 @@ +import { Injectable, InternalServerErrorException, Logger } from '@nestjs/common'; + +import { PrismaService } from '@credebl/prisma-service'; +// eslint-disable-next-line camelcase +import { org_roles } from '@prisma/client'; + +@Injectable() +export class OrgRolesRepository { + constructor(private readonly prisma: PrismaService, private readonly logger: Logger) {} + + // eslint-disable-next-line camelcase + async getRole(roleName: string): Promise { + try { + const roleDetails = await this.prisma.org_roles.findFirst({ + where: { + name: roleName + } + }); + return roleDetails; + } catch (error) { + this.logger.error(`In get role repository: ${JSON.stringify(error)}`); + throw new InternalServerErrorException('Bad Request'); + } + } + + + // eslint-disable-next-line camelcase + async getOrgRoles(): Promise { + try { + const roleDetails = await this.prisma.org_roles.findMany(); + return roleDetails; + } catch (error) { + this.logger.error(`In get org-roles repository: ${JSON.stringify(error)}`); + throw new InternalServerErrorException('Bad Request'); + + } + + } + + // eslint-disable-next-line camelcase + async getOrgRolesByIds(orgRoles: number[]): Promise { + try { + const roleDetails = await this.prisma.org_roles.findMany({ + where: { + id:{ + in:orgRoles + } + }, + select: { + id: true, + name: true, + description: true + } + }); + this.logger.log(`In getroleDetails: ${JSON.stringify(roleDetails)}`); + + return roleDetails; + } catch (error) { + this.logger.error(`In get org-roles repository: ${JSON.stringify(error)}`); + throw new InternalServerErrorException('Bad Request'); + + } + } +} \ No newline at end of file diff --git a/libs/org-roles/src/index.ts b/libs/org-roles/src/index.ts new file mode 100644 index 000000000..50cd30f84 --- /dev/null +++ b/libs/org-roles/src/index.ts @@ -0,0 +1,2 @@ +export * from './org-roles.module'; +export * from './org-roles.service'; diff --git a/libs/org-roles/src/org-roles.module.ts b/libs/org-roles/src/org-roles.module.ts new file mode 100644 index 000000000..a53dff126 --- /dev/null +++ b/libs/org-roles/src/org-roles.module.ts @@ -0,0 +1,11 @@ +import { PrismaService } from '@credebl/prisma-service'; +import { Logger } from '@nestjs/common'; +import { Module } from '@nestjs/common'; +import { OrgRolesRepository } from '../repositories'; +import { OrgRolesService } from './org-roles.service'; + +@Module({ + providers: [OrgRolesService, OrgRolesRepository, Logger, PrismaService], + exports: [OrgRolesService] +}) +export class OrgRolesModule {} diff --git a/libs/org-roles/src/org-roles.service.ts b/libs/org-roles/src/org-roles.service.ts new file mode 100644 index 000000000..dfc99c75e --- /dev/null +++ b/libs/org-roles/src/org-roles.service.ts @@ -0,0 +1,26 @@ +import { Injectable } from '@nestjs/common'; +import { Logger } from '@nestjs/common'; +import { OrgRolesRepository } from '../repositories'; +// eslint-disable-next-line camelcase +import { org_roles } from '@prisma/client'; + +@Injectable() +export class OrgRolesService { + + constructor(private readonly orgRoleRepository: OrgRolesRepository, private readonly logger: Logger) { } + + // eslint-disable-next-line camelcase + async getRole(roleName: string): Promise { + return this.orgRoleRepository.getRole(roleName); + } + + // eslint-disable-next-line camelcase + async getOrgRoles(): Promise { + return this.orgRoleRepository.getOrgRoles(); + } + + // eslint-disable-next-line camelcase + async getOrgRolesByIds(orgRoleIds: number[]): Promise { + return this.orgRoleRepository.getOrgRolesByIds(orgRoleIds); + } +} diff --git a/libs/org-roles/tsconfig.lib.json b/libs/org-roles/tsconfig.lib.json new file mode 100644 index 000000000..c9d40a1d0 --- /dev/null +++ b/libs/org-roles/tsconfig.lib.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "declaration": true, + "outDir": "../../dist/libs/org-roles" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] +} \ No newline at end of file diff --git a/libs/prisma-service/prisma/schema.prisma b/libs/prisma-service/prisma/schema.prisma new file mode 100644 index 000000000..d69a0d6c3 --- /dev/null +++ b/libs/prisma-service/prisma/schema.prisma @@ -0,0 +1,285 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +model user { + id Int @id(map: "PK_cace4a159ff9f2512dd42373760") @default(autoincrement()) + createDateTime DateTime @default(now()) @db.Timestamptz(6) + createdBy Int @default(1) + lastChangedDateTime DateTime @default(now()) @db.Timestamptz(6) + lastChangedBy Int @default(1) + firstName String? @db.VarChar(500) + lastName String? @db.VarChar(500) + email String? @unique(map: "UQ_e12875dfb3b1d92d7d7c5377e22") @db.VarChar(500) + username String? @db.VarChar(500) + password String? @db.VarChar(500) + verificationCode String? @db.VarChar(500) + isEmailVerified Boolean @default(false) + keycloakUserId String? @db.VarChar(500) + clientId String? @db.VarChar(500) + clientSecret String? @db.VarChar(500) + profileImg String? @db.VarChar(1000) + fidoUserId String? @db.VarChar(1000) + isFidoVerified Boolean @default(false) + userOrgRoles user_org_roles[] + userDevices user_devices[] + orgInvitations org_invitations[] +} + +model org_roles { + id Int @id @default(autoincrement()) + name String @unique + description String + userOrgRoles user_org_roles[] + createDateTime DateTime @default(now()) @db.Timestamptz(6) + createdBy Int @default(1) + lastChangedDateTime DateTime @default(now()) @db.Timestamptz(6) + lastChangedBy Int @default(1) + deletedAt DateTime? @db.Timestamp(6) +} + +model user_org_roles { + id Int @id @default(autoincrement()) + userId Int + orgRoleId Int + orgId Int? + organisation organisation? @relation(fields: [orgId], references: [id]) + orgRole org_roles @relation(fields: [orgRoleId], references: [id]) + user user @relation(fields: [userId], references: [id]) +} + +model organisation { + id Int @id @default(autoincrement()) + createDateTime DateTime @default(now()) @db.Timestamptz(6) + createdBy Int @default(1) + lastChangedDateTime DateTime @default(now()) @db.Timestamptz(6) + lastChangedBy Int @default(1) + name String? @db.VarChar(500) + description String? @db.VarChar(500) + logoUrl String? + website String? @db.VarChar + userOrgRoles user_org_roles[] + orgInvitations org_invitations[] + org_agents org_agents[] + connections connections[] + credentials credentials[] + presentations presentations[] + schema schema[] +} + +model org_invitations { + id Int @id @default(autoincrement()) + createDateTime DateTime @default(now()) @db.Timestamptz(6) + createdBy Int @default(1) + lastChangedDateTime DateTime @default(now()) @db.Timestamptz(6) + lastChangedBy Int @default(1) + deletedAt DateTime? @db.Timestamp(6) + userId Int + orgId Int + status String + user user @relation(fields: [userId], references: [id]) + organisation organisation @relation(fields: [orgId], references: [id]) + orgRoles Int[] + email String? +} + +model user_devices { + id Int @id(map: "PK_c9e7e648903a9e537347aba4371") @default(autoincrement()) + createDateTime DateTime @default(now()) @db.Timestamptz(6) + createdBy Int @default(1) + lastChangedDateTime DateTime @default(now()) @db.Timestamptz(6) + lastChangedBy Int @default(1) + devices Json? @default("[]") + credentialId String? @unique(map: "UQ_7c903f5e362fe8fd3d3edba17b5") @db.VarChar + deviceFriendlyName String? @db.VarChar + userId Int? + deletedAt DateTime? @db.Timestamp(6) + authCounter Int @default(0) + user user? @relation(fields: [userId], references: [id], onDelete: NoAction, onUpdate: NoAction, map: "FK_e12ac4f8016243ac71fd2e415af") +} + +model platform_config { + id Int @id @default(autoincrement()) + externalIp String @db.VarChar + lastInternalId String @db.VarChar + username String @db.VarChar + sgApiKey String @db.VarChar + emailFrom String @db.VarChar + apiEndpoint String @db.VarChar + tailsFileServer String @db.VarChar + createDateTime DateTime @default(now()) @db.Timestamptz(6) + createdBy Int @default(1) + lastChangedDateTime DateTime @default(now()) @db.Timestamptz(6) + lastChangedBy Int @default(1) + deletedAt DateTime? @db.Timestamp(6) +} + +model org_agents { + id Int @unique @default(autoincrement()) + createDateTime DateTime @default(now()) @db.Timestamptz(6) + createdBy Int @default(1) + lastChangedDateTime DateTime @default(now()) @db.Timestamptz(6) + lastChangedBy Int @default(1) + orgDid String @db.VarChar + verkey String @db.VarChar + agentEndPoint String @db.VarChar + agentId Int? + isDidPublic Boolean + agentSpinUpStatus Int + agentOptions Bytes? + walletName String? @db.VarChar + tenantId String? + apiKey String? + agentsTypeId Int + orgId Int + orgAgentTypeId Int + agents_type agents_type? @relation(fields: [agentsTypeId], references: [id]) + org_agent_type org_agents_type? @relation(fields: [orgAgentTypeId], references: [id]) + organisation organisation? @relation(fields: [orgId], references: [id]) + agents agents? @relation(fields: [agentId], references: [id]) + agent_invitations agent_invitations[] +} + +model org_agents_type { + id Int @id @default(autoincrement()) + createDateTime DateTime @default(now()) @db.Timestamptz(6) + createdBy Int @default(1) + lastChangedDateTime DateTime @default(now()) @db.Timestamptz(6) + lastChangedBy Int @default(1) + agent String @db.VarChar(500) + org_agents org_agents[] +} + +model agents_type { + id Int @id @default(autoincrement()) + createDateTime DateTime @default(now()) @db.Timestamptz(6) + createdBy Int @default(1) + lastChangedDateTime DateTime @default(now()) @db.Timestamptz(6) + lastChangedBy Int @default(1) + agent String @db.VarChar(500) + org_agents org_agents[] +} + +model ledgers { + id Int @unique @default(autoincrement()) + createDateTime DateTime @default(now()) @db.Timestamptz(6) + createdBy Int @default(1) + lastChangedDateTime DateTime @default(now()) @db.Timestamptz(6) + lastChangedBy Int @default(1) + name String @db.VarChar + networkType String @db.VarChar + poolConfig String @db.VarChar + isActive Boolean + networkString String @db.VarChar + registerDIDEndpoint String @db.VarChar + registerDIDPayload Json? +} + +model agents { + id Int @unique @default(autoincrement()) + createDateTime DateTime @default(now()) @db.Timestamptz(6) + createdBy Int @default(1) + lastChangedDateTime DateTime @default(now()) @db.Timestamptz(6) + lastChangedBy Int @default(1) + name String + org_agents org_agents[] +} + +model schema { + id Int @id(map: "PK_c9e7e648903a9e537347aba4372") @default(autoincrement()) + createDateTime DateTime @default(now()) @db.Timestamptz(6) + createdBy Int @default(1) + lastChangedDateTime DateTime @default(now()) @db.Timestamptz(6) + lastChangedBy Int @default(1) + name String @db.VarChar + version String @db.VarChar + attributes String[] + schemaLedgerId String @db.VarChar + publisherDid String @db.VarChar + ledgerId Int @default(1) + issuerId String @db.VarChar + orgId Int + organisation organisation? @relation(fields: [orgId], references: [id]) +} + +model credential_definition { + id Int @id(map: "PK_c9e7e648903a9e537347aba4373") @default(autoincrement()) + createDateTime DateTime @default(now()) @db.Timestamptz(6) + createdBy Int @default(1) + lastChangedDateTime DateTime @default(now()) @db.Timestamptz(6) + lastChangedBy Int @default(1) + credentialDefinitionId String @db.VarChar + tag String @db.VarChar + schemaLedgerId String @db.VarChar + schemaId Int @default(1) + orgId Int @default(1) + revocable Boolean @default(false) +} + +model shortening_url { + id Int @id @default(autoincrement()) + referenceId String? @db.VarChar(50) + url String? + type String? +} + +model agent_invitations { + id Int @unique @default(autoincrement()) + orgId Int @default(1) + agentId Int @default(1) + connectionInvitation String + multiUse Boolean + createDateTime DateTime @default(now()) @db.Timestamptz(6) + createdBy Int @default(1) + lastChangedDateTime DateTime @default(now()) @db.Timestamptz(6) + lastChangedBy Int @default(1) + org_agents org_agents? @relation(fields: [agentId], references: [id]) +} + +model connections { + id Int @id @default(autoincrement()) + createDateTime DateTime @default(now()) @db.Timestamptz(6) + createdBy Int @default(1) + lastChangedDateTime DateTime @default(now()) @db.Timestamptz(6) + lastChangedBy Int @default(1) + connectionId String @unique + state String + orgDid String + theirLabel String + autoAcceptConnection Boolean + outOfBandId String + orgId Int + organisation organisation? @relation(fields: [orgId], references: [id]) +} + +model credentials { + id Int @id @default(autoincrement()) + createDateTime DateTime @default(now()) @db.Timestamptz(6) + createdBy Int @default(1) + lastChangedDateTime DateTime @default(now()) @db.Timestamptz(6) + lastChangedBy Int @default(1) + connectionId String @unique + threadId String + protocolVersion String + credentialAttributes Json[] + orgId Int + organisation organisation? @relation(fields: [orgId], references: [id]) +} +model presentations { + id Int @unique @default(autoincrement()) + createDateTime DateTime @default(now()) @db.Timestamptz(6) + createdBy Int @default(1) + lastChangedDateTime DateTime @default(now()) @db.Timestamptz(6) + lastChangedBy Int @default(1) + connectionId String @unique + state String? + threadId String? + isVerified Boolean? + orgId Int + organisation organisation @relation(fields: [orgId], references: [id]) +} diff --git a/libs/prisma-service/prisma/seed.ts b/libs/prisma-service/prisma/seed.ts new file mode 100644 index 000000000..e171980f2 --- /dev/null +++ b/libs/prisma-service/prisma/seed.ts @@ -0,0 +1,123 @@ +import * as fs from 'fs'; + +import { Logger } from '@nestjs/common'; +import { PrismaClient } from '@prisma/client'; + +// import {} from './data/' +const prisma = new PrismaClient(); +const logger = new Logger('Init seed DB'); + +const configData = fs.readFileSync(`${process.env.PWD}/prisma/data/credebl-master-table.json`, 'utf8'); +const createPlatformConfig = async (): Promise => { + try { + const { platformConfigData } = JSON.parse(configData); + const platformConfig = await prisma.platform_config.create({ + data: platformConfigData + }); + + logger.log(platformConfig); + } catch (e) { + logger.error('An error occurred seeding platformConfig:', e); + } +}; + +const createOrgRoles = async (): Promise => { + try { + const { orgRoleData } = JSON.parse(configData); + const orgRoles = await prisma.org_roles.createMany({ + data: orgRoleData + }); + + logger.log(orgRoles); + } catch (e) { + logger.error('An error occurred seeding orgRoles:', e); + } +}; + +const createAgentTypes = async (): Promise => { + try { + const { agentTypeData } = JSON.parse(configData); + const agentTypes = await prisma.agents_type.createMany({ + data: agentTypeData + }); + + logger.log(agentTypes); + } catch (e) { + logger.error('An error occurred seeding agentTypes:', e); + } +}; + +const createOrgAgentTypes = async (): Promise => { + try { + const { orgAgentTypeData } = JSON.parse(configData); + const orgAgentTypes = await prisma.org_agents_type.createMany({ + data: orgAgentTypeData + }); + + logger.log(orgAgentTypes); + } catch (e) { + logger.error('An error occurred seeding orgAgentTypes:', e); + } +}; + +const createPlatformUser = async (): Promise => { + try { + const { platformAdminData } = JSON.parse(configData); + const platformUser = await prisma.user.create({ + data: platformAdminData + }); + + logger.log(platformUser); + } catch (e) { + logger.error('An error occurred seeding platformUser:', e); + } +}; + + +const createPlatformOrganization = async (): Promise => { + try { + const { platformAdminOrganizationData } = JSON.parse(configData); + const platformOrganization = await prisma.organisation.create({ + data: platformAdminOrganizationData + }); + + logger.log(platformOrganization); + } catch (e) { + logger.error('An error occurred seeding platformOrganization:', e); + } +}; + +const createPlatformUserOrgRoles = async (): Promise => { + try { + const { userOrgRoleData } = JSON.parse(configData); + const platformOrganization = await prisma.user_org_roles.create({ + data: userOrgRoleData + }); + + logger.log(platformOrganization); + } catch (e) { + logger.error('An error occurred seeding platformOrganization:', e); + } +}; + +async function main(): Promise { + + await createPlatformConfig(); + await createOrgRoles(); + await createAgentTypes(); + await createPlatformOrganization(); + await createPlatformUser(); + await createPlatformUserOrgRoles(); + await createOrgAgentTypes(); +} + + +main() + .then(async () => { + await prisma.$disconnect(); + }) + .catch(async (e) => { + logger.error(`In prisma seed initialize`, e); + await prisma.$disconnect(); + process.exit(1); + }); diff --git a/libs/prisma-service/src/index.ts b/libs/prisma-service/src/index.ts new file mode 100644 index 000000000..cd925ece3 --- /dev/null +++ b/libs/prisma-service/src/index.ts @@ -0,0 +1,2 @@ +export * from './prisma-service.module'; +export * from './prisma-service.service'; diff --git a/libs/prisma-service/src/prisma-service.module.ts b/libs/prisma-service/src/prisma-service.module.ts new file mode 100644 index 000000000..73c39b1d7 --- /dev/null +++ b/libs/prisma-service/src/prisma-service.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { PrismaService } from './prisma-service.service'; + +@Module({ + providers: [PrismaService], + exports: [PrismaService] +}) +export class PrismaServiceModule {} diff --git a/libs/prisma-service/src/prisma-service.service.ts b/libs/prisma-service/src/prisma-service.service.ts new file mode 100644 index 000000000..5acc4e3d1 --- /dev/null +++ b/libs/prisma-service/src/prisma-service.service.ts @@ -0,0 +1,13 @@ +import { INestApplication, Injectable, OnModuleInit } from '@nestjs/common'; +import { PrismaClient } from '@prisma/client'; + +@Injectable() +export class PrismaService extends PrismaClient implements OnModuleInit { + UserDevicesRepository: any; + async onModuleInit(): Promise { + await this.$connect(); + } + + async enableShutdownHooks(app: INestApplication): Promise { + } +} \ No newline at end of file diff --git a/libs/prisma-service/tsconfig.lib.json b/libs/prisma-service/tsconfig.lib.json new file mode 100644 index 000000000..db04b298e --- /dev/null +++ b/libs/prisma-service/tsconfig.lib.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "declaration": true, + "outDir": "../../dist/libs/prisma-service" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] +} diff --git a/libs/push-notifications/src/index.ts b/libs/push-notifications/src/index.ts new file mode 100644 index 000000000..ae3deb101 --- /dev/null +++ b/libs/push-notifications/src/index.ts @@ -0,0 +1,2 @@ +export * from './push-notifications.module'; +export * from './push-notifications.service'; diff --git a/libs/push-notifications/src/push-notifications.module.ts b/libs/push-notifications/src/push-notifications.module.ts new file mode 100644 index 000000000..c8673f5ba --- /dev/null +++ b/libs/push-notifications/src/push-notifications.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { PushNotificationsService } from './push-notifications.service'; + +@Module({ + providers: [PushNotificationsService], + exports: [PushNotificationsService] +}) +export class PushNotificationsModule {} diff --git a/libs/push-notifications/src/push-notifications.service.spec.ts b/libs/push-notifications/src/push-notifications.service.spec.ts new file mode 100644 index 000000000..96ba9eed4 --- /dev/null +++ b/libs/push-notifications/src/push-notifications.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { PushNotificationsService } from './push-notifications.service'; + +describe('PushNotificationsService', () => { + let service: PushNotificationsService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [PushNotificationsService] + }).compile(); + + service = module.get(PushNotificationsService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/libs/push-notifications/src/push-notifications.service.ts b/libs/push-notifications/src/push-notifications.service.ts new file mode 100644 index 000000000..21b4b6836 --- /dev/null +++ b/libs/push-notifications/src/push-notifications.service.ts @@ -0,0 +1,42 @@ +import { Injectable, Logger } from '@nestjs/common'; +// import * as firebase from 'firebase-admin'; + +@Injectable() +export class PushNotificationsService { + + private logger = new Logger('PushNotificationsService'); + + public pushNotification(firebaseToken: string, payload: any, options: any) { + // set file path from server where firebase configuration file + // if (firebaseToken) { + // return firebase.messaging().sendToDevice(firebaseToken, payload, options) + // .then(response => { + // this.logger.log(`Notification sent successfully${JSON.stringify(response)}`); + // }) + // .catch(error => { + // this.logger.error(error); + // return `notification sent ${JSON.stringify(error)}`; + // }); + // } + } + + + public webPushNotification(firebaseToken: string, payload: any) { + // set file path from server where firebase configuration file + const options = { + priority: 'high', + timeToLive: 60 * 69 * 24 + }; + // if (firebaseToken) { + // return firebase.messaging().sendToDevice(firebaseToken, payload, options) + // .then(response => { + // this.logger.debug(`\nfirebaseToken: ${firebaseToken} \n payload: ${JSON.stringify(payload)}`); + // this.logger.log(`Web Notification sent successfully${JSON.stringify(response)}`); + // }) + // .catch(error => { + // this.logger.error(error); + // return `notification sent ${JSON.stringify(error)}`; + // }); + // } + } +} diff --git a/libs/push-notifications/tsconfig.lib.json b/libs/push-notifications/tsconfig.lib.json new file mode 100644 index 000000000..7ef7bee0b --- /dev/null +++ b/libs/push-notifications/tsconfig.lib.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "declaration": true, + "outDir": "../../dist/libs/push-notifications" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] +} diff --git a/libs/response/src/index.ts b/libs/response/src/index.ts new file mode 100644 index 000000000..e3c993e24 --- /dev/null +++ b/libs/response/src/index.ts @@ -0,0 +1,2 @@ +export * from './response.module'; +export * from './response.service'; diff --git a/libs/response/src/response.module.ts b/libs/response/src/response.module.ts new file mode 100644 index 000000000..b45d3e058 --- /dev/null +++ b/libs/response/src/response.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { ResponseService } from './response.service'; + +@Module({ + providers: [ResponseService], + exports: [ResponseService] +}) +export class ResponseModule {} diff --git a/libs/response/src/response.service.spec.ts b/libs/response/src/response.service.spec.ts new file mode 100644 index 000000000..5a2127211 --- /dev/null +++ b/libs/response/src/response.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ResponseService } from './response.service'; + +describe('ResponseService', () => { + let service: ResponseService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ResponseService] + }).compile(); + + service = module.get(ResponseService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/libs/response/src/response.service.ts b/libs/response/src/response.service.ts new file mode 100644 index 000000000..f986a7ee9 --- /dev/null +++ b/libs/response/src/response.service.ts @@ -0,0 +1,23 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class ResponseService { + + message: string; + data: any; + success: boolean; + code: number; + + public response(message: string, success: boolean, data?: any, code?: number): ResponseService { + // This function should be static so no need to create object in every method not changing code because of + // does not know impact of it on how many function and files. + //Todo: function should be static. + + const response: ResponseService = new ResponseService(); + response.message = message; + response.data = data; + response.success = success; + response.code = code; + return response; + } +} diff --git a/libs/response/tsconfig.lib.json b/libs/response/tsconfig.lib.json new file mode 100644 index 000000000..6898d2ebb --- /dev/null +++ b/libs/response/tsconfig.lib.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "declaration": true, + "outDir": "../../dist/libs/response" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] +} diff --git a/libs/service/base.service.ts b/libs/service/base.service.ts new file mode 100644 index 000000000..c7250c087 --- /dev/null +++ b/libs/service/base.service.ts @@ -0,0 +1,62 @@ +import { HttpException, Logger } from '@nestjs/common'; + +import { ClientProxy } from '@nestjs/microservices'; +import { map } from 'rxjs/operators'; + +export class BaseService { + protected logger; + + constructor(loggerName: string) { + this.logger = new Logger(loggerName); + } + + sendNats(serviceProxy: ClientProxy, cmd: string, payload: any): Promise { + + const startTs = Date.now(); + const pattern = { cmd }; + + return serviceProxy + .send(pattern, payload) + .pipe( + map((response: string) => ({ + response + //duration: Date.now() - startTs, + })) + ) + .toPromise() + .catch((error) => { + this.logger.error(`catch: ${JSON.stringify(error)}`); + if (error && error.message) { + throw new HttpException( + { + status: error.statusCode, + error: error.message + }, + error.statusCode + ); + } else if (error) { + throw new HttpException( + { + status: error.statusCode, + error: error.error + }, + error.statusCode + + ); + } else { + this.logger.error( + `The error received was in an unexpected format. Returning generic 500 error... ${JSON.stringify( + error + )}` + ); + throw new HttpException( + { + status: 500, + error: error.message + }, + 500 + ); + } + }); + } +} diff --git a/libs/service/nats.options.ts b/libs/service/nats.options.ts new file mode 100644 index 000000000..6bea3c62d --- /dev/null +++ b/libs/service/nats.options.ts @@ -0,0 +1,16 @@ +import { NatsOptions, Transport } from '@nestjs/microservices'; +export const commonNatsOptions = (name: string, isClient = true) => { + const common: NatsOptions = { + transport: Transport.NATS, + options: { + url: `nats://${process.env.NATS_HOST}:${process.env.NATS_PORT}`, + name, + maxReconnectAttempts: -1, + reconnectTimeWait: 3000 + } + }; + const result = isClient + ? { ...common, options: { ...common.options, reconnect: true } } + : common; + return result; +}; diff --git a/libs/user-org-roles/repositories/index.ts b/libs/user-org-roles/repositories/index.ts new file mode 100644 index 000000000..ad18b094b --- /dev/null +++ b/libs/user-org-roles/repositories/index.ts @@ -0,0 +1,99 @@ +import { Injectable, Logger } from '@nestjs/common'; + +import { InternalServerErrorException } from '@nestjs/common'; +import { PrismaService } from '@credebl/prisma-service'; +// eslint-disable-next-line camelcase +import { user_org_roles } from '@prisma/client'; +import { Prisma } from '@prisma/client'; + +type UserOrgRolesWhereUniqueInput = Prisma.user_org_rolesWhereUniqueInput; + +@Injectable() +export class UserOrgRolesRepository { + constructor(private readonly prisma: PrismaService, private readonly logger: Logger) {} + + /** + * + * @param createUserDto + * @returns user details + */ + // eslint-disable-next-line camelcase + async createUserOrgRole(userId: number, roleId: number, orgId?: number): Promise { + try { + const data: { + orgRole: { connect: { id: number } }; + user: { connect: { id: number } }; + organisation?: { connect: { id: number } }; + } = { + orgRole: { connect: { id: roleId } }, + user: { connect: { id: userId } } + }; + + if (orgId) { + data.organisation = { connect: { id: orgId } }; + } + + const saveResponse = await this.prisma.user_org_roles.create({ + data + }); + + return saveResponse; + } catch (error) { + this.logger.error(`UserOrgRolesRepository:: createUserOrgRole: ${error}`); + throw new InternalServerErrorException('User Org Role not created'); + } + } + + /** + * + * @param + * @returns organizations details + */ + // eslint-disable-next-line camelcase + async getUserOrgData(queryOptions: object): Promise { + try { + return this.prisma.user_org_roles.findMany({ + where: { + ...queryOptions + }, + include: { + organisation: { + include: { + // eslint-disable-next-line camelcase + org_agents: true, + orgInvitations: true + } + }, + orgRole: true + } + }); + } catch (error) { + this.logger.error(`error: ${JSON.stringify(error)}`); + throw new InternalServerErrorException(error); + } + } + + async findAndUpdate(queryOptions: UserOrgRolesWhereUniqueInput, updateData: object): Promise { + try { + return this.prisma.user_org_roles.update({ + where: { ...queryOptions }, + data: { ...updateData } + }); + } catch (error) { + this.logger.error(`error in findAndUpdate: ${JSON.stringify(error)}`); + throw new InternalServerErrorException(error); + } + } + + async deleteMany(queryOptions: object): Promise { + try { + return this.prisma.user_org_roles.deleteMany({ + where: { ...queryOptions } + }); + } catch (error) { + this.logger.error(`error in deleteMany: ${JSON.stringify(error)}`); + throw new InternalServerErrorException(error); + } + } + +} diff --git a/libs/user-org-roles/src/index.ts b/libs/user-org-roles/src/index.ts new file mode 100644 index 000000000..be7c557e1 --- /dev/null +++ b/libs/user-org-roles/src/index.ts @@ -0,0 +1,2 @@ +export * from './user-org-roles.module'; +export * from './user-org-roles.service'; diff --git a/libs/user-org-roles/src/user-org-roles.module.ts b/libs/user-org-roles/src/user-org-roles.module.ts new file mode 100644 index 000000000..be4c0b84f --- /dev/null +++ b/libs/user-org-roles/src/user-org-roles.module.ts @@ -0,0 +1,11 @@ +import { PrismaService } from '@credebl/prisma-service'; +import { Logger } from '@nestjs/common'; +import { Module } from '@nestjs/common'; +import { UserOrgRolesRepository } from '../repositories'; +import { UserOrgRolesService } from './user-org-roles.service'; + +@Module({ + providers: [UserOrgRolesService, UserOrgRolesRepository, Logger, PrismaService], + exports: [UserOrgRolesService] +}) +export class UserOrgRolesModule {} diff --git a/libs/user-org-roles/src/user-org-roles.service.ts b/libs/user-org-roles/src/user-org-roles.service.ts new file mode 100644 index 000000000..842af78f3 --- /dev/null +++ b/libs/user-org-roles/src/user-org-roles.service.ts @@ -0,0 +1,74 @@ +import { Injectable } from '@nestjs/common'; +import { UserOrgRolesRepository } from '../repositories'; +// eslint-disable-next-line camelcase +import { user_org_roles } from '@prisma/client'; + +@Injectable() +export class UserOrgRolesService { + constructor(private readonly userOrgRoleRepository: UserOrgRolesRepository) {} + + /** + * + * @param createUserDto + * @returns user details + */ + // eslint-disable-next-line camelcase + async createUserOrgRole(userId: number, roleId: number, orgId?: number): Promise { + return this.userOrgRoleRepository.createUserOrgRole(userId, roleId, orgId); + } + + + /** + * + * @param userId + * @param orgId + * @returns Boolean response for user exist + */ + async checkUserOrgExist(userId: number, orgId: number): Promise { + const queryOptions = { + userId, + orgId + }; + const userOrgDetails = await this.userOrgRoleRepository.getUserOrgData(queryOptions); + + if (userOrgDetails && 0 === userOrgDetails.length) { + return false; + } + + return true; + } + + + /** + * + * @param userId + * @param orgId + * @param roleIds + * @returns + */ + async updateUserOrgRole(userId: number, orgId: number, roleIds: number[]): Promise { + + for (const role of roleIds) { + this.userOrgRoleRepository.createUserOrgRole(userId, role, orgId); + } + + return true; + } + + /** + * + * @param userId + * @param orgId + * @returns Delete user org roles + */ + async deleteOrgRoles(userId: number, orgId: number): Promise { + + const queryOptions = { + userId, + orgId + }; + + return this.userOrgRoleRepository.deleteMany(queryOptions); + + } +} diff --git a/libs/user-org-roles/tsconfig.lib.json b/libs/user-org-roles/tsconfig.lib.json new file mode 100644 index 000000000..3cb249bc1 --- /dev/null +++ b/libs/user-org-roles/tsconfig.lib.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "declaration": true, + "outDir": "../../dist/libs/user-org-roles" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] +} diff --git a/libs/user-request/src/index.ts b/libs/user-request/src/index.ts new file mode 100644 index 000000000..0799fc9ba --- /dev/null +++ b/libs/user-request/src/index.ts @@ -0,0 +1,2 @@ +export * from './user-request.module'; +export * from './user-request.service'; diff --git a/libs/user-request/src/user-request.interface.ts b/libs/user-request/src/user-request.interface.ts new file mode 100644 index 000000000..25712574f --- /dev/null +++ b/libs/user-request/src/user-request.interface.ts @@ -0,0 +1,57 @@ +export interface IUserRequest { + userId: number; + email: string; + orgId: number; + agentEndPoint?: string; + apiKey?: string; + tenantId?: number; + tenantName?: string; + tenantOrgId?: number; + userRoleOrgPermissions?: IUserRoleOrgPerms[]; + orgName?: string; + selectedOrg: ISelectedOrg; +} + +export interface ISelectedOrg { + id: number; + userId: number; + orgRoleId: number; + orgId: number; + orgRole: object; + organisation: object; +} + +export interface IOrganization { + name: string; + description: string; + org_agents: IOrgAgent[] + +} + +export interface IOrgAgent { + orgDid: string; + verkey: string; + agentEndPoint: string; + agentOptions: string; + walletName: string; + agentsTypeId: string; + orgId: string; +} + +export class IUserRoleOrgPerms { + id: number; + role: IUserRole; + Organization: IUserOrg; +} + +export class IUserRole { + id: number; + name: string; + permissions: string[]; + +} + +export class IUserOrg { + id: number; + orgName: string; +} \ No newline at end of file diff --git a/libs/user-request/src/user-request.module.ts b/libs/user-request/src/user-request.module.ts new file mode 100644 index 000000000..b1b50ffd8 --- /dev/null +++ b/libs/user-request/src/user-request.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { UserRequestService } from './user-request.service'; + +@Module({ + providers: [UserRequestService], + exports: [UserRequestService] +}) +export class UserRequestModule {} diff --git a/libs/user-request/src/user-request.service.spec.ts b/libs/user-request/src/user-request.service.spec.ts new file mode 100644 index 000000000..acc591d94 --- /dev/null +++ b/libs/user-request/src/user-request.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { UserRequestService } from './user-request.service'; + +describe('UserRequestService', () => { + let service: UserRequestService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [UserRequestService] + }).compile(); + + service = module.get(UserRequestService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/libs/user-request/src/user-request.service.ts b/libs/user-request/src/user-request.service.ts new file mode 100644 index 000000000..1aa672185 --- /dev/null +++ b/libs/user-request/src/user-request.service.ts @@ -0,0 +1,4 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class UserRequestService {} diff --git a/libs/user-request/tsconfig.lib.json b/libs/user-request/tsconfig.lib.json new file mode 100644 index 000000000..7d511c9d7 --- /dev/null +++ b/libs/user-request/tsconfig.lib.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "declaration": true, + "outDir": "../../dist/libs/user-request" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] +} diff --git a/nest-cli.json b/nest-cli.json new file mode 100644 index 000000000..0cc2ab8c6 --- /dev/null +++ b/nest-cli.json @@ -0,0 +1,219 @@ +{ + "collection": "@nestjs/schematics", + "sourceRoot": "apps/api-gateway/src", + "monorepo": true, + "root": "apps/api-gateway", + "compilerOptions": { + "webpack": true, + "tsConfigPath": "apps/api-gateway/tsconfig.app.json" + }, + "projects": { + "api-gateway": { + "type": "application", + "root": "apps/api-gateway", + "entryFile": "main", + "sourceRoot": "apps/api-gateway/src", + "compilerOptions": { + "tsConfigPath": "apps/api-gateway/tsconfig.app.json" + } + }, + "platform-service": { + "type": "application", + "root": "apps/platform-service", + "entryFile": "main", + "sourceRoot": "apps/platform-service/src", + "compilerOptions": { + "tsConfigPath": "apps/platform-service/tsconfig.app.json" + } + }, + "response": { + "type": "library", + "root": "libs/response", + "entryFile": "index", + "sourceRoot": "libs/response/src", + "compilerOptions": { + "tsConfigPath": "libs/response/tsconfig.lib.json" + } + }, + "common": { + "type": "library", + "root": "libs/common", + "entryFile": "index", + "sourceRoot": "libs/common/src", + "compilerOptions": { + "tsConfigPath": "libs/common/tsconfig.lib.json" + } + }, + "push-notifications": { + "type": "library", + "root": "libs/push-notifications", + "entryFile": "index", + "sourceRoot": "libs/push-notifications/src", + "compilerOptions": { + "tsConfigPath": "libs/push-notifications/tsconfig.lib.json" + } + }, + "keycloak-url": { + "type": "library", + "root": "libs/keycloak-url", + "entryFile": "index", + "sourceRoot": "libs/keycloak-url/src", + "compilerOptions": { + "tsConfigPath": "libs/keycloak-url/tsconfig.lib.json" + } + }, + "client-registration": { + "type": "library", + "root": "libs/client-registration", + "entryFile": "index", + "sourceRoot": "libs/client-registration/src", + "compilerOptions": { + "tsConfigPath": "libs/client-registration/tsconfig.lib.json" + } + }, + "connection": { + "type": "application", + "root": "apps/connection", + "entryFile": "main", + "sourceRoot": "apps/connection/src", + "compilerOptions": { + "tsConfigPath": "apps/connection/tsconfig.app.json" + } + }, + "prisma": { + "type": "library", + "root": "libs/prisma", + "entryFile": "index", + "sourceRoot": "libs/prisma/src", + "compilerOptions": { + "tsConfigPath": "libs/prisma/tsconfig.lib.json" + } + }, + "repositories": { + "type": "library", + "root": "libs/repositories", + "entryFile": "index", + "sourceRoot": "libs/repositories/src", + "compilerOptions": { + "tsConfigPath": "libs/repositories/tsconfig.lib.json" + } + }, + "user-request": { + "type": "library", + "root": "libs/user-request", + "entryFile": "index", + "sourceRoot": "libs/user-request/src", + "compilerOptions": { + "tsConfigPath": "libs/user-request/tsconfig.lib.json" + } + }, + "logger": { + "type": "library", + "root": "libs/logger", + "entryFile": "index", + "sourceRoot": "libs/logger/src", + "compilerOptions": { + "tsConfigPath": "libs/logger/tsconfig.lib.json" + } + }, + "enum": { + "type": "library", + "root": "libs/enum", + "entryFile": "index", + "sourceRoot": "libs/enum/src", + "compilerOptions": { + "tsConfigPath": "libs/enum/tsconfig.lib.json" + } + }, + "prisma-service": { + "type": "library", + "root": "libs/prisma-service", + "entryFile": "index", + "sourceRoot": "libs/prisma-service/src", + "compilerOptions": { + "tsConfigPath": "libs/prisma-service/tsconfig.lib.json" + } + }, + "organization": { + "type": "application", + "root": "apps/organization", + "entryFile": "main", + "sourceRoot": "apps/organization/src", + "compilerOptions": { + "tsConfigPath": "apps/organization/tsconfig.app.json" + } + }, + "user": { + "type": "application", + "root": "apps/user", + "entryFile": "main", + "sourceRoot": "apps/user/src", + "compilerOptions": { + "tsConfigPath": "apps/user/tsconfig.app.json" + } + }, + "org-roles": { + "type": "library", + "root": "libs/org-roles", + "entryFile": "index", + "sourceRoot": "libs/org-roles/src", + "compilerOptions": { + "tsConfigPath": "libs/org-roles/tsconfig.lib.json" + } + }, + "user-org-roles": { + "type": "library", + "root": "libs/user-org-roles", + "entryFile": "index", + "sourceRoot": "libs/user-org-roles/src", + "compilerOptions": { + "tsConfigPath": "libs/user-org-roles/tsconfig.lib.json" + } + }, + "ledger": { + "type": "application", + "root": "apps/ledger", + "entryFile": "main", + "sourceRoot": "apps/ledger/src", + "compilerOptions": { + "tsConfigPath": "apps/ledger/tsconfig.app.json" + } + }, + "agent-service": { + "type": "application", + "root": "apps/agent-service", + "entryFile": "main", + "sourceRoot": "apps/agent-service/src", + "compilerOptions": { + "tsConfigPath": "apps/agent-service/tsconfig.app.json" + } + }, + "agent-provisioning": { + "type": "application", + "root": "apps/agent-provisioning", + "entryFile": "main", + "sourceRoot": "apps/agent-provisioning/src", + "compilerOptions": { + "tsConfigPath": "apps/agent-provisioning/tsconfig.app.json" + } + }, + "issuance": { + "type": "application", + "root": "apps/issuance", + "entryFile": "main", + "sourceRoot": "apps/issuance/src", + "compilerOptions": { + "tsConfigPath": "apps/issuance/tsconfig.app.json" + } + }, + "verification": { + "type": "application", + "root": "apps/verification", + "entryFile": "main", + "sourceRoot": "apps/verification/src", + "compilerOptions": { + "tsConfigPath": "apps/verification/tsconfig.app.json" + } + } + } +} \ No newline at end of file diff --git a/package.json b/package.json new file mode 100755 index 000000000..9d97ca433 --- /dev/null +++ b/package.json @@ -0,0 +1,170 @@ +{ + "name": "api-gateway", + "version": "0.0.1", + "description": "CREDEBL SSI Platform API Gateway", + "author": "", + "license": "MIT", + "scripts": { + "prebuild": "rimraf dist", + "build": "nest build", + "build:all": "rimraf dist && nest build api-gateway && nest build platform-service && nest build agent-service && nest build connection", + "format": "prettier --write \"apps/**/*.ts\" \"libs/**/*.ts\"", + "start": "nest start", + "start:dev": "nest start --watch", + "start:debug": "nest start --debug --watch", + "start:prod": "node dist/apps/api-gateway/main", + "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", + "test": "jest", + "test:watch": "jest --watch", + "test:cov": "jest --coverage", + "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", + "test:e2e": "jest --config ./apps/api-gateway/test/jest-e2e.json", + "prisma:generate": "npx prisma generate", + "prepare": "husky install" + }, + "prisma": { + "seed": "ts-node prisma/seed.ts" + }, + "dependencies": { + "@nestjs/axios": "^3.0.0", + "@nestjs/bull": "^10.0.1", + "@nestjs/common": "^10.1.3", + "@nestjs/config": "^3.0.0", + "@nestjs/core": "^10.1.3", + "@nestjs/jwt": "^10.1.0", + "@nestjs/microservices": "^10.1.3", + "@nestjs/passport": "^10.0.0", + "@nestjs/platform-express": "^10.1.3", + "@nestjs/platform-socket.io": "^10.1.3", + "@nestjs/schedule": "^3.0.1", + "@nestjs/swagger": "^7.1.6", + "@nestjs/typeorm": "^10.0.0", + "@nestjs/websockets": "^10.1.3", + "@prisma/client": "^5.1.0", + "@sendgrid/mail": "^7.7.0", + "@types/crypto-js": "^4.1.1", + "@types/pdfkit": "^0.12.6", + "auth0-js": "^9.22.1", + "bcrypt": "^5.1.0", + "blob-stream": "^0.1.3", + "body-parser": "^1.20.1", + "buffer": "^6.0.3", + "bull": "^4.10.2", + "cache-manager": "^5.1.3", + "cache-manager-redis-store": "^2.0.0", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.0", + "crypto-js": "^4.1.1", + "crypto-random-string": "^5.0.0", + "dotenv": "^16.0.3", + "express": "^4.18.2", + "fs": "0.0.1-security", + "generate-password": "^1.7.0", + "html-pdf": "^3.0.1", + "json2csv": "^5.0.7", + "jsonwebtoken": "^9.0.1", + "jwks-rsa": "^3.0.1", + "moment": "^2.29.3", + "nanoid": "^4.0.2", + "nats": "^2.15.1", + "nestjs-typeorm-paginate": "^4.0.4", + "node-qpdf2": "^2.0.0", + "papaparse": "^5.4.1", + "passport": "^0.6.0", + "passport-jwt": "^4.0.1", + "passport-local": "^1.0.0", + "path": "^0.12.7", + "pdfkit": "^0.13.0", + "pg": "^8.11.2", + "qrcode": "^1.5.1", + "qs": "^6.11.2", + "reflect-metadata": "^0.1.13", + "rimraf": "^3.0.2", + "rsync": "^0.6.1", + "rxjs": "^7.8.1", + "socket.io-client": "^4.7.1", + "swagger-ui-express": "^5.0.0", + "typeorm": "^0.3.10", + "unzipper": "^0.10.14", + "uuid": "^9.0.0", + "web-push": "^3.6.4", + "xml-js": "^1.6.11" + }, + "devDependencies": { + "@nestjs/cli": "^10.1.11", + "@nestjs/schematics": "^10.0.1", + "@nestjs/testing": "^10.1.3", + "@types/bull": "^4.10.0", + "@types/cron": "^2.4.0", + "@types/express": "^4.17.17", + "@types/jest": "^29.5.3", + "@types/multer": "^1.4.7", + "@types/node": "^20.4.5", + "@types/passport-jwt": "3.0.9", + "@types/passport-local": "^1.0.35", + "@types/socket.io": "^3.0.2", + "@types/supertest": "^2.0.12", + "@typescript-eslint/eslint-plugin": "^6.2.1", + "@typescript-eslint/parser": "^6.2.1", + "eslint": "^8.46.0", + "eslint-config-prettier": "^8.5.0", + "eslint-config-standard-with-typescript": "^37.0.0", + "eslint-plugin-import": "^2.27.5", + "eslint-plugin-n": "^15.7.0", + "eslint-plugin-prettier": "^4.2.1", + "eslint-plugin-promise": "^6.1.1", + "husky": "^8.0.3", + "jest": "^29.6.2", + "lint-staged": "^13.2.3", + "prettier": "^3.0.0", + "prisma": "^5.1.0", + "supertest": "^6.3.3", + "ts-jest": "^29.1.1", + "ts-loader": "^9.4.4", + "ts-node": "^10.9.1", + "tsconfig-paths": "^4.2.0", + "typescript": "^5.1.6" + }, + "lint-staged": { + "apps/**/*.{ts}": "prettier --write", + "apps/**/*.ts": "eslint" + }, + "jest": { + "moduleFileExtensions": [ + "js", + "json", + "ts" + ], + "rootDir": ".", + "testRegex": ".spec.ts$", + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + }, + "coverageDirectory": "./coverage", + "testEnvironment": "node", + "roots": [ + "/apps/", + "/libs/" + ], + "moduleNameMapper": { + "@credebl/responses/(.*)": "/libs/responses/src/$1", + "@credebl/responses": "/libs/responses/src", + "@credebl/common/(.*)": "/libs/common/src/$1", + "@credebl/common": "/libs/common/src", + "@credebl/push-notifications/(.*)": "/libs/push-notifications/src/$1", + "@credebl/push-notifications": "/libs/push-notifications/src", + "@credebl/keycloak-url/(.*)": "/libs/keycloak-url/src/$1", + "@credebl/keycloak-url": "/libs/keycloak-url/src", + "@credebl/client-registration/(.*)": "/libs/client-registration/src/$1", + "@credebl/client-registration": "/libs/client-registration/src", + "^@credebl/prisma(|/.*)$": "/libs/prisma/src/$1", + "^@credebl/repositories(|/.*)$": "/libs/repositories/src/$1", + "^@credebl/user-request(|/.*)$": "/libs/user-request/src/$1", + "^@credebl/logger(|/.*)$": "/libs/logger/src/$1", + "^@credebl/enum(|/.*)$": "/libs/enum/src/$1", + "^@credebl/prisma-service(|/.*)$": "/libs/prisma-service/src/$1", + "^@credebl/org-roles(|/.*)$": "/libs/org-roles/src/$1", + "^@credebl/user-org-roles(|/.*)$": "/libs/user-org-roles/src/$1" + } + } +} diff --git a/resources/Blcokster_logo.png b/resources/Blcokster_logo.png new file mode 100644 index 000000000..0785c86cb Binary files /dev/null and b/resources/Blcokster_logo.png differ diff --git a/resources/CREDEBLEmail.png b/resources/CREDEBLEmail.png new file mode 100644 index 000000000..47fd7d112 Binary files /dev/null and b/resources/CREDEBLEmail.png differ diff --git a/resources/CREDEBL_Logo.png b/resources/CREDEBL_Logo.png new file mode 100644 index 000000000..db7628678 Binary files /dev/null and b/resources/CREDEBL_Logo.png differ diff --git a/resources/Credebl_logo_with_slogan.svg b/resources/Credebl_logo_with_slogan.svg new file mode 100644 index 000000000..b5acaca1f --- /dev/null +++ b/resources/Credebl_logo_with_slogan.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/resources/Seropasspng-01.png b/resources/Seropasspng-01.png new file mode 100644 index 000000000..a47cfef67 Binary files /dev/null and b/resources/Seropasspng-01.png differ diff --git a/resources/check-mark-old.png b/resources/check-mark-old.png new file mode 100644 index 000000000..5b802855c Binary files /dev/null and b/resources/check-mark-old.png differ diff --git a/resources/check-mark.png b/resources/check-mark.png new file mode 100644 index 000000000..d5beb4964 Binary files /dev/null and b/resources/check-mark.png differ diff --git a/resources/checked.svg b/resources/checked.svg new file mode 100644 index 000000000..777ad864e --- /dev/null +++ b/resources/checked.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/resources/credebl-old.jpg b/resources/credebl-old.jpg new file mode 100644 index 000000000..39a554e36 Binary files /dev/null and b/resources/credebl-old.jpg differ diff --git a/resources/credebl.jpg b/resources/credebl.jpg new file mode 100644 index 000000000..39a554e36 Binary files /dev/null and b/resources/credebl.jpg differ diff --git a/resources/credebl.png b/resources/credebl.png new file mode 100644 index 000000000..c85edc6c5 Binary files /dev/null and b/resources/credebl.png differ diff --git a/resources/deal-old.png b/resources/deal-old.png new file mode 100644 index 000000000..93a7e11da Binary files /dev/null and b/resources/deal-old.png differ diff --git a/resources/deal.png b/resources/deal.png new file mode 100644 index 000000000..350f4b539 Binary files /dev/null and b/resources/deal.png differ diff --git a/resources/invite.png b/resources/invite.png new file mode 100644 index 000000000..366cd1af9 Binary files /dev/null and b/resources/invite.png differ diff --git a/resources/lock-old.png b/resources/lock-old.png new file mode 100644 index 000000000..e51a1d079 Binary files /dev/null and b/resources/lock-old.png differ diff --git a/resources/lock.png b/resources/lock.png new file mode 100644 index 000000000..7c800f154 Binary files /dev/null and b/resources/lock.png differ diff --git a/resources/mobie-otp.svg b/resources/mobie-otp.svg new file mode 100644 index 000000000..d16da1551 --- /dev/null +++ b/resources/mobie-otp.svg @@ -0,0 +1,256 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/mobile-otp.png b/resources/mobile-otp.png new file mode 100644 index 000000000..c8b934761 Binary files /dev/null and b/resources/mobile-otp.png differ diff --git a/resources/otp-image.svg b/resources/otp-image.svg new file mode 100644 index 000000000..b5e26eb84 --- /dev/null +++ b/resources/otp-image.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/platform-admn-logo.png b/resources/platform-admn-logo.png new file mode 100644 index 000000000..d0889f9b3 Binary files /dev/null and b/resources/platform-admn-logo.png differ diff --git a/resources/qr_code.svg b/resources/qr_code.svg new file mode 100644 index 000000000..c10b62924 --- /dev/null +++ b/resources/qr_code.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/resources/register-image.png b/resources/register-image.png new file mode 100644 index 000000000..edff2da34 Binary files /dev/null and b/resources/register-image.png differ diff --git a/resources/reset-password.svg b/resources/reset-password.svg new file mode 100644 index 000000000..221b05309 --- /dev/null +++ b/resources/reset-password.svg @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/seropass.png b/resources/seropass.png new file mode 100644 index 000000000..6c3c35167 Binary files /dev/null and b/resources/seropass.png differ diff --git a/resources/shield.png b/resources/shield.png new file mode 100644 index 000000000..5479acaed Binary files /dev/null and b/resources/shield.png differ diff --git a/resources/verification-image.png b/resources/verification-image.png new file mode 100644 index 000000000..882a307b1 Binary files /dev/null and b/resources/verification-image.png differ diff --git a/resources/verification-image.svg b/resources/verification-image.svg new file mode 100644 index 000000000..8d6bed949 --- /dev/null +++ b/resources/verification-image.svg @@ -0,0 +1,220 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/verify-email.svg b/resources/verify-email.svg new file mode 100644 index 000000000..e8ef5cbfa --- /dev/null +++ b/resources/verify-email.svg @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/welcome.svg b/resources/welcome.svg new file mode 100644 index 000000000..a81d525be --- /dev/null +++ b/resources/welcome.svg @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/welcomeotp.png b/resources/welcomeotp.png new file mode 100644 index 000000000..56cf05ad2 Binary files /dev/null and b/resources/welcomeotp.png differ diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 000000000..64f86c6bd --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 000000000..aeec9b0f3 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,98 @@ +{ + "compilerOptions": { + "module": "commonjs", + "declaration": true, + "removeComments": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "target": "es2017", + "sourceMap": true, + "outDir": "./dist", + "baseUrl": "./", + "incremental": true, + "paths": { + "@credebl/response": [ + "libs/response/src" + ], + "@credebl/response/*": [ + "libs/response/src/*" + ], + "@credebl/common": [ + "libs/common/src" + ], + "@credebl/common/*": [ + "libs/common/src/*" + ], + "@credebl/push-notifications": [ + "libs/push-notifications/src" + ], + "@credebl/push-notifications/*": [ + "libs/push-notifications/src/*" + ], + "@credebl/keycloak-url": [ + "libs/keycloak-url/src" + ], + "@credebl/keycloak-url/*": [ + "libs/keycloak-url/src/*" + ], + "@credebl/client-registration": [ + "libs/client-registration/src" + ], + "@credebl/client-registration/*": [ + "libs/client-registration/src/*" + ], + "@credebl/prisma": [ + "libs/prisma/src" + ], + "@credebl/prisma/*": [ + "libs/prisma/src/*" + ], + "@credebl/repositories": [ + "libs/repositories/src" + ], + "@credebl/repositories/*": [ + "libs/repositories/src/*" + ], + "@credebl/user-request": [ + "libs/user-request/src" + ], + "@credebl/user-request/*": [ + "libs/user-request/src/*" + ], + "@credebl/logger": [ + "libs/logger/src" + ], + "@credebl/logger/*": [ + "libs/logger/src/*" + ], + "@credebl/enum": [ + "libs/enum/src" + ], + "@credebl/enum/*": [ + "libs/enum/src/*" + ], + "@credebl/prisma-service": [ + "libs/prisma-service/src" + ], + "@credebl/prisma-service/*": [ + "libs/prisma-service/src/*" + ], + "@credebl/org-roles": [ + "libs/org-roles/src" + ], + "@credebl/org-roles/*": [ + "libs/org-roles/src/*" + ], + "@credebl/user-org-roles": [ + "libs/user-org-roles/src" + ], + "@credebl/user-org-roles/*": [ + "libs/user-org-roles/src/*" + ] + } + }, + "exclude": [ + "node_modules", + "dist" + ] +} \ No newline at end of file