From 1e09b10503ea6c7153fbff3673cc06727411b1e1 Mon Sep 17 00:00:00 2001 From: "pranav.laygude@ayanworks.com" Date: Thu, 3 Aug 2023 19:23:20 +0530 Subject: [PATCH] initial commit Signed-off-by: pranav.laygude@ayanworks.com --- .env.sample | 111 +++ .eslintignore | 0 .eslintrc.js | 104 +++ .github/ISSUE_TEMPLATE/bug_report.md | 31 + .gitignore | 136 +--- .gitmodules | 3 + .husky/pre-commit | 4 + .nvmrc | 1 + .prettierrc | 9 + README-Microservice.md | 18 + README.md | 101 ++- .../AFJ/scripts/start_agent.sh | 160 ++++ .../AFJ/scripts/start_agent_ecs.sh | 209 +++++ apps/agent-provisioning/Dockerfile | 53 ++ .../src/agent-provisioning.controller.ts | 19 + .../src/agent-provisioning.module.ts | 23 + .../src/agent-provisioning.service.ts | 52 ++ .../agent-provisioning.interfaces.ts | 66 ++ apps/agent-provisioning/src/main.ts | 22 + apps/agent-provisioning/test/app.e2e-spec.ts | 22 + apps/agent-provisioning/test/jest-e2e.json | 9 + apps/agent-provisioning/tsconfig.app.json | 9 + apps/agent-service/Dockerfile | 43 + .../src/agent-service.controller.ts | 91 +++ .../agent-service/src/agent-service.module.ts | 29 + .../src/agent-service.service.ts | 745 ++++++++++++++++++ .../src/interface/agent-service.interface.ts | 275 +++++++ apps/agent-service/src/main.ts | 39 + .../repositories/agent-service.repository.ts | 137 ++++ apps/agent-service/test/app.e2e-spec.ts | 24 + apps/agent-service/test/jest-e2e.json | 9 + apps/agent-service/tsconfig.app.json | 9 + apps/api-gateway/Dockerfile | 42 + apps/api-gateway/common/exception-handler.ts | 49 ++ apps/api-gateway/common/interface.ts | 6 + .../agent-service.controller.spec.ts | 18 + .../agent-service/agent-service.controller.ts | 96 +++ .../src/agent-service/agent-service.module.ts | 28 + .../agent-service/agent-service.service.ts | 26 + .../src/agent-service/agent.service.spec.ts | 18 + .../agent-service/dto/agent-service.dto.ts | 71 ++ .../agent-service/dto/create-schema.dto.ts | 27 + .../agent-service/dto/create-tenant.dto.ts | 31 + .../src/agent/agent.controller.spec.ts | 18 + .../api-gateway/src/agent/agent.controller.ts | 289 +++++++ apps/api-gateway/src/agent/agent.module.ts | 26 + .../src/agent/agent.service.spec.ts | 18 + apps/api-gateway/src/agent/agent.service.ts | 95 +++ apps/api-gateway/src/app.controller.spec.ts | 17 + apps/api-gateway/src/app.controller.ts | 10 + apps/api-gateway/src/app.module.ts | 87 ++ apps/api-gateway/src/app.service.ts | 12 + .../api-gateway/src/authz/authz.controller.ts | 17 + .../api-gateway/src/authz/authz.middleware.ts | 131 +++ apps/api-gateway/src/authz/authz.module.ts | 57 ++ apps/api-gateway/src/authz/authz.service.ts | 28 + .../authz/decorators/get-user.decorator.ts | 7 + .../src/authz/decorators/roles.decorator.ts | 9 + .../src/authz/decorators/user.decorator.ts | 8 + .../src/authz/dtos/auth-token-res.dto.ts | 21 + .../src/authz/dtos/client-login.dto.ts | 10 + .../src/authz/dtos/firebase-token.dto.ts | 8 + .../src/authz/dtos/requesting-user.dto.ts | 20 + .../src/authz/dtos/user-login.dto.ts | 12 + .../src/authz/dtos/user-role-org-perms.dto.ts | 19 + .../src/authz/guards/org-roles.guard.ts | 55 ++ .../src/authz/jwt-payload.interface.ts | 12 + apps/api-gateway/src/authz/jwt.strategy.ts | 60 ++ .../src/authz/mobile-jwt.strategy.ts | 51 ++ apps/api-gateway/src/authz/roles.guard.ts | 55 ++ apps/api-gateway/src/authz/socket.gateway.ts | 105 +++ apps/api-gateway/src/config/multer.config.ts | 25 + .../src/connection/connection.controller.ts | 199 +++++ .../src/connection/connection.module.ts | 24 + .../src/connection/connection.service.ts | 61 ++ .../src/connection/dtos/connection.dto.ts | 100 +++ .../src/connection/enums/connections.enum.ts | 12 + .../src/connection/interfaces/index.ts | 55 ++ .../credential-definition.controller.spec.ts | 204 +++++ .../credential-definition.controller.ts | 102 +++ .../credential-definition.module.ts | 27 + .../credential-definition.service.spec.ts | 18 + .../credential-definition.service.ts | 46 ++ .../dto/create-cred-defs.dto.ts | 32 + .../dto/get-all-cred-defs.dto.ts | 47 ++ .../credential-definition/interfaces/index.ts | 41 + apps/api-gateway/src/dtos/PresentProof.dto.ts | 3 + .../src/dtos/UpdateNonAdminUser.dto.ts | 15 + .../src/dtos/admin-onboard-user.dto.ts | 67 ++ .../src/dtos/admin-profile-update.dto.ts | 49 ++ .../src/dtos/apiResponse.dto copy.ts | 15 + apps/api-gateway/src/dtos/apiResponse.dto.ts | 15 + .../src/dtos/approval-status.dto.ts | 7 + apps/api-gateway/src/dtos/authDto.dto.ts | 21 + .../src/dtos/bad-request-error.dto.ts | 14 + apps/api-gateway/src/dtos/category.dto.ts | 22 + .../src/dtos/connection-out-of-band.dto.ts | 28 + apps/api-gateway/src/dtos/connection.dto.ts | 49 ++ .../dtos/create-credential-definition.dto.ts | 32 + .../src/dtos/create-feature-price.dto.ts | 31 + .../src/dtos/create-proof-request.dto.ts | 20 + .../dtos/create-revocation-registry.dto.ts | 13 + .../api-gateway/src/dtos/create-schema.dto.ts | 29 + .../src/dtos/createEnterprise-query.dto.ts | 38 + .../src/dtos/created-response-dto.ts | 16 + .../src/dtos/credential-offer.dto.ts | 40 + .../src/dtos/credential-send-offer.dto.ts | 10 + apps/api-gateway/src/dtos/email.dto.ts | 18 + .../src/dtos/endorse-transaction.dto.ts | 6 + apps/api-gateway/src/dtos/enums.ts | 51 ++ apps/api-gateway/src/dtos/fido-user.dto.ts | 136 ++++ .../src/dtos/forbidden-error.dto copy.ts | 10 + .../src/dtos/forbidden-error.dto.ts | 10 + .../src/dtos/forgot-password.dto.ts | 17 + .../api-gateway/src/dtos/get-cred-defs.dto.ts | 10 + .../src/dtos/holder-details.dto.ts | 67 ++ .../src/dtos/holder-reset-password.ts | 21 + .../src/dtos/internal-server-error-res.dto.ts | 10 + .../src/dtos/issue-credential-offer.dto .ts | 64 ++ .../dtos/issue-credential-out-of-band.dto.ts | 61 ++ .../src/dtos/issue-credential-save.dto.ts | 69 ++ .../src/dtos/issue-credential.dto.ts | 40 + apps/api-gateway/src/dtos/label-editor.dto.ts | 12 + .../src/dtos/not-found-error.dto.ts | 22 + .../src/dtos/org-name-check.dto.ts | 16 + .../src/dtos/platform-config.dto.ts | 21 + .../src/dtos/platform-connection.dtos.ts | 27 + .../src/dtos/printable-form-details.dto.ts | 12 + .../src/dtos/register-non-admin-user.dto.ts | 40 + .../src/dtos/register-tenant.dto.ts | 81 ++ .../api-gateway/src/dtos/register-user.dto.ts | 110 +++ .../src/dtos/remote-get-credential.dto.ts | 16 + .../api-gateway/src/dtos/remove-holder.dto.ts | 13 + .../src/dtos/reset-password.dto.ts | 18 + .../src/dtos/revoke-credential.dto.ts | 36 + .../src/dtos/save-roles-permissions.dto.ts | 14 + apps/api-gateway/src/dtos/schemasearch.dto.ts | 37 + .../src/dtos/send-invite-toOrg.dto.ts | 25 + .../src/dtos/send-proof-request.dto.ts | 26 + apps/api-gateway/src/dtos/subscription.dto.ts | 10 + .../src/dtos/unauthorized-error.dto.ts | 10 + .../src/dtos/update-profile.dto.ts | 21 + .../dtos/update-revocation-registry.dto.ts | 10 + apps/api-gateway/src/dtos/user-counts.dto.ts | 9 + .../src/dtos/user-profile-update.dto.ts | 14 + .../src/dtos/user-role-org-perms.dto.ts | 18 + .../src/dtos/wallet-details.dto.ts | 17 + apps/api-gateway/src/enum.ts | 118 +++ .../api-gateway/src/fido/dto/fido-user.dto.ts | 103 +++ apps/api-gateway/src/fido/fido.controller.ts | 229 ++++++ apps/api-gateway/src/fido/fido.module.ts | 21 + apps/api-gateway/src/fido/fido.service.ts | 58 ++ .../src/interfaces/ISchemaSearch.interface.ts | 18 + .../src/interfaces/ISocket.interface.ts | 9 + .../src/interfaces/IUserRequestInterface.ts | 15 + .../src/interfaces/fileExport.interface.ts | 13 + .../src/issuance/dtos/issuance.dto.ts | 98 +++ .../src/issuance/enums/Issuance.enum.ts | 13 + .../src/issuance/interfaces/index.ts | 57 ++ .../src/issuance/issuance.controller.ts | 207 +++++ .../src/issuance/issuance.module.ts | 24 + .../src/issuance/issuance.service.ts | 53 ++ apps/api-gateway/src/main.ts | 73 ++ .../dtos/create-organization-dto.ts | 36 + .../dtos/get-all-organizations.dto.ts | 27 + .../dtos/get-all-sent-invitations.dto.ts | 26 + .../organization/dtos/send-invitation.dto.ts | 37 + .../dtos/update-organization-dto.ts | 40 + .../dtos/update-user-roles.dto.ts | 26 + .../organization/organization.controller.ts | 209 +++++ .../src/organization/organization.module.ts | 29 + .../src/organization/organization.service.ts | 133 ++++ .../src/platform/platform.controller.spec.ts | 74 ++ .../src/platform/platform.controller.ts | 13 + .../src/platform/platform.interface.ts | 33 + .../src/platform/platform.module.ts | 22 + .../src/platform/platform.service.ts | 65 ++ .../src/revocation/revocation.controller.ts | 79 ++ .../src/revocation/revocation.module.ts | 23 + .../src/revocation/revocation.service.ts | 46 ++ .../src/schema/dtos/create-schema.dto.ts | 28 + .../src/schema/dtos/get-all-schema.dto.ts | 68 ++ .../src/schema/interfaces/index.ts | 41 + .../src/schema/schema.controller.spec.ts | 222 ++++++ .../src/schema/schema.controller.ts | 151 ++++ apps/api-gateway/src/schema/schema.module.ts | 24 + apps/api-gateway/src/schema/schema.service.ts | 62 ++ .../src/secrets/13b0e1df87be249a.pem | 36 + apps/api-gateway/src/secrets/idswallet.key | 28 + .../user/dto/accept-reject-invitation.dto.ts | 28 + apps/api-gateway/src/user/dto/add-user.dto.ts | 27 + .../src/user/dto/create-user.dto.ts | 15 + .../src/user/dto/email-verify.dto.ts | 21 + .../src/user/dto/get-all-invitations.dto.ts | 32 + .../src/user/dto/get-all-users.dto.ts | 26 + .../src/user/dto/login-user.dto.ts | 51 ++ apps/api-gateway/src/user/interfaces/index.ts | 23 + apps/api-gateway/src/user/user.controller.ts | 268 +++++++ apps/api-gateway/src/user/user.module.ts | 26 + apps/api-gateway/src/user/user.service.ts | 96 +++ .../src/verification/dto/request-proof.dto.ts | 48 ++ .../src/verification/dto/webhook-proof.dto.ts | 55 ++ .../interfaces/verification.interface.ts | 7 + .../verification/verification.controller.ts | 209 +++++ .../src/verification/verification.module.ts | 23 + .../src/verification/verification.service.ts | 67 ++ apps/api-gateway/test/app.e2e-spec.ts | 22 + apps/api-gateway/test/jest-e2e.json | 9 + apps/api-gateway/tsconfig.app.json | 9 + apps/connection/Dockerfile | 43 + apps/connection/src/connection.controller.ts | 53 ++ apps/connection/src/connection.module.ts | 26 + apps/connection/src/connection.repository.ts | 149 ++++ apps/connection/src/connection.service.ts | 303 +++++++ apps/connection/src/enum.ts | 4 + .../src/interfaces/connection.interfaces.ts | 82 ++ apps/connection/src/main.ts | 23 + apps/connection/test/app.e2e-spec.ts | 22 + apps/connection/test/jest-e2e.json | 9 + apps/connection/tsconfig.app.json | 9 + apps/issuance/Dockerfile | 43 + .../interfaces/issuance.interfaces.ts | 45 ++ apps/issuance/src/issuance.controller.ts | 40 + apps/issuance/src/issuance.module.ts | 27 + apps/issuance/src/issuance.repository.ts | 126 +++ apps/issuance/src/issuance.service.ts | 305 +++++++ apps/issuance/src/main.ts | 23 + apps/issuance/tsconfig.app.json | 9 + apps/ledger/Dockerfile | 43 + .../credential-definition.controller.ts | 48 ++ .../credential-definition.module.ts | 33 + .../credential-definition.service.ts | 259 ++++++ .../create-credential-definition.interface.ts | 53 ++ .../credential-definition.interface.ts | 31 + .../credential-definition/interfaces/index.ts | 41 + .../credential-definition.repository.ts | 155 ++++ apps/ledger/src/ledger.controller.spec.ts | 22 + apps/ledger/src/ledger.controller.ts | 8 + apps/ledger/src/ledger.module.ts | 25 + apps/ledger/src/ledger.service.ts | 5 + apps/ledger/src/main.ts | 20 + .../schema/dtos/user-role-org-perms.dto.ts | 27 + .../interfaces/schema-payload.interface.ts | 59 ++ .../src/schema/interfaces/schema.interface.ts | 41 + .../schema/repositories/schema.repository.ts | 196 +++++ apps/ledger/src/schema/schema.controller.ts | 67 ++ apps/ledger/src/schema/schema.interface.ts | 41 + apps/ledger/src/schema/schema.module.ts | 34 + apps/ledger/src/schema/schema.service.ts | 353 +++++++++ apps/ledger/tsconfig.app.json | 9 + apps/organization/Dockerfile | 43 + .../dtos/create-organization.dto.ts | 15 + apps/organization/dtos/send-invitation.dto.ts | 13 + .../organization/dtos/update-invitation.dt.ts | 7 + .../dtos/verify-email-token.dto.ts | 8 + .../interfaces/organization.interface.ts | 21 + .../repositories/organization.repository.ts | 397 ++++++++++ apps/organization/src/main.ts | 22 + .../src/organization.controller.ts | 126 +++ apps/organization/src/organization.module.ts | 31 + apps/organization/src/organization.service.ts | 379 +++++++++ .../organization-invitation.template.ts | 73 ++ .../organization-onboard.template.ts | 85 ++ .../templates/organization-url-template.ts | 81 ++ apps/organization/tsconfig.app.json | 9 + apps/user/Dockerfile | 43 + .../user/dtos/accept-reject-invitation.dto.ts | 7 + apps/user/dtos/create-user.dto.ts | 6 + apps/user/dtos/keycloak-register.dto.ts | 29 + apps/user/dtos/login-user.dto.ts | 21 + apps/user/dtos/verify-email.dto.ts | 4 + apps/user/interfaces/user.interface.ts | 44 ++ .../user/repositories/fido-user.repository.ts | 132 ++++ .../repositories/user-device.repository.ts | 290 +++++++ apps/user/repositories/user.repository.ts | 334 ++++++++ apps/user/src/fido/dtos/fido-user.dto.ts | 162 ++++ apps/user/src/fido/fido.controller.ts | 86 ++ apps/user/src/fido/fido.module.ts | 51 ++ apps/user/src/fido/fido.service.ts | 235 ++++++ apps/user/src/main.ts | 22 + apps/user/src/user.controller.ts | 90 +++ apps/user/src/user.module.ts | 46 ++ apps/user/src/user.service.ts | 516 ++++++++++++ apps/user/templates/user-onboard.template.ts | 85 ++ apps/user/templates/user-url-template.ts | 81 ++ apps/user/test/app.e2e-spec.ts | 24 + apps/user/test/jest-e2e.json | 9 + apps/user/tsconfig.app.json | 9 + apps/verification/Dockerfile | 43 + .../src/interfaces/verification.interface.ts | 99 +++ apps/verification/src/main.ts | 23 + .../repositories/verification.repository.ts | 66 ++ .../src/verification.controller.ts | 56 ++ apps/verification/src/verification.module.ts | 26 + apps/verification/src/verification.service.ts | 450 +++++++++++ apps/verification/tsconfig.app.json | 9 + docker-compose.yml | 23 + .../src/client-registration.module.ts | 8 + .../src/client-registration.service.spec.ts | 18 + .../src/client-registration.service.ts | 661 ++++++++++++++++ .../src/dtos/accessTokenPayloadDto.ts | 7 + .../client-credential-token-payload.dto.ts | 7 + .../src/dtos/create-user.dto.ts | 23 + .../src/dtos/userTokenPayloadDto.ts | 9 + libs/client-registration/src/index.ts | 2 + libs/client-registration/tsconfig.lib.json | 9 + libs/common/src/cast.helper.ts | 47 ++ libs/common/src/common.constant.ts | 326 ++++++++ libs/common/src/common.module.ts | 11 + libs/common/src/common.service.spec.ts | 18 + libs/common/src/common.service.ts | 328 ++++++++ libs/common/src/common.utils.ts | 41 + libs/common/src/dtos/email.dto.ts | 7 + libs/common/src/exception-handler.ts | 69 ++ libs/common/src/index.ts | 2 + libs/common/src/interfaces/interface.ts | 7 + .../src/interfaces/response.interface.ts | 6 + libs/common/src/response-messages/index.ts | 159 ++++ libs/common/src/send-grid-helper-file.ts | 26 + libs/common/tsconfig.lib.json | 9 + libs/entities/base.number.entity.ts | 19 + libs/entities/base.system.entity.ts | 25 + libs/enum/src/enum.module.ts | 8 + libs/enum/src/enum.service.spec.ts | 18 + libs/enum/src/enum.service.ts | 4 + libs/enum/src/enum.ts | 19 + libs/enum/src/index.ts | 2 + libs/enum/tsconfig.lib.json | 9 + libs/http-exception.filter.ts | 15 + libs/keycloak-url/src/index.ts | 2 + libs/keycloak-url/src/keycloak-url.module.ts | 8 + libs/keycloak-url/src/keycloak-url.service.ts | 74 ++ libs/keycloak-url/tsconfig.lib.json | 9 + libs/nest-cli.json | 16 + libs/org-roles/enums/index.ts | 9 + libs/org-roles/repositories/index.ts | 64 ++ libs/org-roles/src/index.ts | 2 + libs/org-roles/src/org-roles.module.ts | 11 + libs/org-roles/src/org-roles.service.ts | 26 + libs/org-roles/tsconfig.lib.json | 9 + libs/prisma-service/prisma/schema.prisma | 285 +++++++ libs/prisma-service/prisma/seed.ts | 123 +++ libs/prisma-service/src/index.ts | 2 + .../src/prisma-service.module.ts | 8 + .../src/prisma-service.service.ts | 13 + libs/prisma-service/tsconfig.lib.json | 9 + libs/push-notifications/src/index.ts | 2 + .../src/push-notifications.module.ts | 8 + .../src/push-notifications.service.spec.ts | 18 + .../src/push-notifications.service.ts | 42 + libs/push-notifications/tsconfig.lib.json | 9 + libs/response/src/index.ts | 2 + libs/response/src/response.module.ts | 8 + libs/response/src/response.service.spec.ts | 18 + libs/response/src/response.service.ts | 23 + libs/response/tsconfig.lib.json | 9 + libs/service/base.service.ts | 62 ++ libs/service/nats.options.ts | 16 + libs/user-org-roles/repositories/index.ts | 99 +++ libs/user-org-roles/src/index.ts | 2 + .../src/user-org-roles.module.ts | 11 + .../src/user-org-roles.service.ts | 74 ++ libs/user-org-roles/tsconfig.lib.json | 9 + libs/user-request/src/index.ts | 2 + .../src/user-request.interface.ts | 57 ++ libs/user-request/src/user-request.module.ts | 8 + .../src/user-request.service.spec.ts | 18 + libs/user-request/src/user-request.service.ts | 4 + libs/user-request/tsconfig.lib.json | 9 + nest-cli.json | 219 +++++ package.json | 170 ++++ resources/Blcokster_logo.png | Bin 0 -> 5766 bytes resources/CREDEBLEmail.png | Bin 0 -> 4920 bytes resources/CREDEBL_Logo.png | Bin 0 -> 3050 bytes resources/Credebl_logo_with_slogan.svg | 9 + resources/Seropasspng-01.png | Bin 0 -> 7174 bytes resources/check-mark-old.png | Bin 0 -> 18338 bytes resources/check-mark.png | Bin 0 -> 901 bytes resources/checked.svg | 9 + resources/credebl-old.jpg | Bin 0 -> 10549 bytes resources/credebl.jpg | Bin 0 -> 10549 bytes resources/credebl.png | Bin 0 -> 42296 bytes resources/deal-old.png | Bin 0 -> 12991 bytes resources/deal.png | Bin 0 -> 1233 bytes resources/invite.png | Bin 0 -> 7822 bytes resources/lock-old.png | Bin 0 -> 10684 bytes resources/lock.png | Bin 0 -> 1114 bytes resources/mobie-otp.svg | 256 ++++++ resources/mobile-otp.png | Bin 0 -> 12796 bytes resources/otp-image.svg | 50 ++ resources/platform-admn-logo.png | Bin 0 -> 2538 bytes resources/qr_code.svg | 9 + resources/register-image.png | Bin 0 -> 10356 bytes resources/reset-password.svg | 73 ++ resources/seropass.png | Bin 0 -> 4870 bytes resources/shield.png | Bin 0 -> 1212 bytes resources/verification-image.png | Bin 0 -> 16583 bytes resources/verification-image.svg | 220 ++++++ resources/verify-email.svg | 90 +++ resources/welcome.svg | 75 ++ resources/welcomeotp.png | Bin 0 -> 13247 bytes tsconfig.build.json | 4 + tsconfig.json | 98 +++ 403 files changed, 22325 insertions(+), 131 deletions(-) create mode 100644 .env.sample create mode 100644 .eslintignore create mode 100644 .eslintrc.js create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .gitmodules create mode 100755 .husky/pre-commit create mode 100644 .nvmrc create mode 100644 .prettierrc create mode 100644 README-Microservice.md create mode 100755 apps/agent-provisioning/AFJ/scripts/start_agent.sh create mode 100644 apps/agent-provisioning/AFJ/scripts/start_agent_ecs.sh create mode 100644 apps/agent-provisioning/Dockerfile create mode 100644 apps/agent-provisioning/src/agent-provisioning.controller.ts create mode 100644 apps/agent-provisioning/src/agent-provisioning.module.ts create mode 100644 apps/agent-provisioning/src/agent-provisioning.service.ts create mode 100644 apps/agent-provisioning/src/interface/agent-provisioning.interfaces.ts create mode 100644 apps/agent-provisioning/src/main.ts create mode 100644 apps/agent-provisioning/test/app.e2e-spec.ts create mode 100644 apps/agent-provisioning/test/jest-e2e.json create mode 100644 apps/agent-provisioning/tsconfig.app.json create mode 100644 apps/agent-service/Dockerfile create mode 100644 apps/agent-service/src/agent-service.controller.ts create mode 100644 apps/agent-service/src/agent-service.module.ts create mode 100644 apps/agent-service/src/agent-service.service.ts create mode 100644 apps/agent-service/src/interface/agent-service.interface.ts create mode 100644 apps/agent-service/src/main.ts create mode 100644 apps/agent-service/src/repositories/agent-service.repository.ts create mode 100644 apps/agent-service/test/app.e2e-spec.ts create mode 100644 apps/agent-service/test/jest-e2e.json create mode 100644 apps/agent-service/tsconfig.app.json create mode 100644 apps/api-gateway/Dockerfile create mode 100644 apps/api-gateway/common/exception-handler.ts create mode 100644 apps/api-gateway/common/interface.ts create mode 100644 apps/api-gateway/src/agent-service/agent-service.controller.spec.ts create mode 100644 apps/api-gateway/src/agent-service/agent-service.controller.ts create mode 100644 apps/api-gateway/src/agent-service/agent-service.module.ts create mode 100644 apps/api-gateway/src/agent-service/agent-service.service.ts create mode 100644 apps/api-gateway/src/agent-service/agent.service.spec.ts create mode 100644 apps/api-gateway/src/agent-service/dto/agent-service.dto.ts create mode 100644 apps/api-gateway/src/agent-service/dto/create-schema.dto.ts create mode 100644 apps/api-gateway/src/agent-service/dto/create-tenant.dto.ts create mode 100644 apps/api-gateway/src/agent/agent.controller.spec.ts create mode 100644 apps/api-gateway/src/agent/agent.controller.ts create mode 100644 apps/api-gateway/src/agent/agent.module.ts create mode 100644 apps/api-gateway/src/agent/agent.service.spec.ts create mode 100644 apps/api-gateway/src/agent/agent.service.ts create mode 100644 apps/api-gateway/src/app.controller.spec.ts create mode 100644 apps/api-gateway/src/app.controller.ts create mode 100644 apps/api-gateway/src/app.module.ts create mode 100644 apps/api-gateway/src/app.service.ts create mode 100644 apps/api-gateway/src/authz/authz.controller.ts create mode 100644 apps/api-gateway/src/authz/authz.middleware.ts create mode 100644 apps/api-gateway/src/authz/authz.module.ts create mode 100644 apps/api-gateway/src/authz/authz.service.ts create mode 100644 apps/api-gateway/src/authz/decorators/get-user.decorator.ts create mode 100644 apps/api-gateway/src/authz/decorators/roles.decorator.ts create mode 100644 apps/api-gateway/src/authz/decorators/user.decorator.ts create mode 100644 apps/api-gateway/src/authz/dtos/auth-token-res.dto.ts create mode 100644 apps/api-gateway/src/authz/dtos/client-login.dto.ts create mode 100644 apps/api-gateway/src/authz/dtos/firebase-token.dto.ts create mode 100644 apps/api-gateway/src/authz/dtos/requesting-user.dto.ts create mode 100644 apps/api-gateway/src/authz/dtos/user-login.dto.ts create mode 100644 apps/api-gateway/src/authz/dtos/user-role-org-perms.dto.ts create mode 100644 apps/api-gateway/src/authz/guards/org-roles.guard.ts create mode 100644 apps/api-gateway/src/authz/jwt-payload.interface.ts create mode 100644 apps/api-gateway/src/authz/jwt.strategy.ts create mode 100644 apps/api-gateway/src/authz/mobile-jwt.strategy.ts create mode 100644 apps/api-gateway/src/authz/roles.guard.ts create mode 100644 apps/api-gateway/src/authz/socket.gateway.ts create mode 100644 apps/api-gateway/src/config/multer.config.ts create mode 100644 apps/api-gateway/src/connection/connection.controller.ts create mode 100644 apps/api-gateway/src/connection/connection.module.ts create mode 100644 apps/api-gateway/src/connection/connection.service.ts create mode 100644 apps/api-gateway/src/connection/dtos/connection.dto.ts create mode 100644 apps/api-gateway/src/connection/enums/connections.enum.ts create mode 100644 apps/api-gateway/src/connection/interfaces/index.ts create mode 100644 apps/api-gateway/src/credential-definition/credential-definition.controller.spec.ts create mode 100644 apps/api-gateway/src/credential-definition/credential-definition.controller.ts create mode 100644 apps/api-gateway/src/credential-definition/credential-definition.module.ts create mode 100644 apps/api-gateway/src/credential-definition/credential-definition.service.spec.ts create mode 100644 apps/api-gateway/src/credential-definition/credential-definition.service.ts create mode 100644 apps/api-gateway/src/credential-definition/dto/create-cred-defs.dto.ts create mode 100644 apps/api-gateway/src/credential-definition/dto/get-all-cred-defs.dto.ts create mode 100644 apps/api-gateway/src/credential-definition/interfaces/index.ts create mode 100644 apps/api-gateway/src/dtos/PresentProof.dto.ts create mode 100644 apps/api-gateway/src/dtos/UpdateNonAdminUser.dto.ts create mode 100644 apps/api-gateway/src/dtos/admin-onboard-user.dto.ts create mode 100644 apps/api-gateway/src/dtos/admin-profile-update.dto.ts create mode 100644 apps/api-gateway/src/dtos/apiResponse.dto copy.ts create mode 100644 apps/api-gateway/src/dtos/apiResponse.dto.ts create mode 100644 apps/api-gateway/src/dtos/approval-status.dto.ts create mode 100644 apps/api-gateway/src/dtos/authDto.dto.ts create mode 100644 apps/api-gateway/src/dtos/bad-request-error.dto.ts create mode 100644 apps/api-gateway/src/dtos/category.dto.ts create mode 100644 apps/api-gateway/src/dtos/connection-out-of-band.dto.ts create mode 100644 apps/api-gateway/src/dtos/connection.dto.ts create mode 100644 apps/api-gateway/src/dtos/create-credential-definition.dto.ts create mode 100644 apps/api-gateway/src/dtos/create-feature-price.dto.ts create mode 100644 apps/api-gateway/src/dtos/create-proof-request.dto.ts create mode 100644 apps/api-gateway/src/dtos/create-revocation-registry.dto.ts create mode 100644 apps/api-gateway/src/dtos/create-schema.dto.ts create mode 100644 apps/api-gateway/src/dtos/createEnterprise-query.dto.ts create mode 100644 apps/api-gateway/src/dtos/created-response-dto.ts create mode 100644 apps/api-gateway/src/dtos/credential-offer.dto.ts create mode 100644 apps/api-gateway/src/dtos/credential-send-offer.dto.ts create mode 100644 apps/api-gateway/src/dtos/email.dto.ts create mode 100644 apps/api-gateway/src/dtos/endorse-transaction.dto.ts create mode 100644 apps/api-gateway/src/dtos/enums.ts create mode 100644 apps/api-gateway/src/dtos/fido-user.dto.ts create mode 100644 apps/api-gateway/src/dtos/forbidden-error.dto copy.ts create mode 100644 apps/api-gateway/src/dtos/forbidden-error.dto.ts create mode 100644 apps/api-gateway/src/dtos/forgot-password.dto.ts create mode 100644 apps/api-gateway/src/dtos/get-cred-defs.dto.ts create mode 100644 apps/api-gateway/src/dtos/holder-details.dto.ts create mode 100644 apps/api-gateway/src/dtos/holder-reset-password.ts create mode 100644 apps/api-gateway/src/dtos/internal-server-error-res.dto.ts create mode 100644 apps/api-gateway/src/dtos/issue-credential-offer.dto .ts create mode 100644 apps/api-gateway/src/dtos/issue-credential-out-of-band.dto.ts create mode 100644 apps/api-gateway/src/dtos/issue-credential-save.dto.ts create mode 100644 apps/api-gateway/src/dtos/issue-credential.dto.ts create mode 100644 apps/api-gateway/src/dtos/label-editor.dto.ts create mode 100644 apps/api-gateway/src/dtos/not-found-error.dto.ts create mode 100644 apps/api-gateway/src/dtos/org-name-check.dto.ts create mode 100644 apps/api-gateway/src/dtos/platform-config.dto.ts create mode 100644 apps/api-gateway/src/dtos/platform-connection.dtos.ts create mode 100644 apps/api-gateway/src/dtos/printable-form-details.dto.ts create mode 100644 apps/api-gateway/src/dtos/register-non-admin-user.dto.ts create mode 100644 apps/api-gateway/src/dtos/register-tenant.dto.ts create mode 100644 apps/api-gateway/src/dtos/register-user.dto.ts create mode 100644 apps/api-gateway/src/dtos/remote-get-credential.dto.ts create mode 100644 apps/api-gateway/src/dtos/remove-holder.dto.ts create mode 100644 apps/api-gateway/src/dtos/reset-password.dto.ts create mode 100644 apps/api-gateway/src/dtos/revoke-credential.dto.ts create mode 100644 apps/api-gateway/src/dtos/save-roles-permissions.dto.ts create mode 100644 apps/api-gateway/src/dtos/schemasearch.dto.ts create mode 100644 apps/api-gateway/src/dtos/send-invite-toOrg.dto.ts create mode 100644 apps/api-gateway/src/dtos/send-proof-request.dto.ts create mode 100644 apps/api-gateway/src/dtos/subscription.dto.ts create mode 100644 apps/api-gateway/src/dtos/unauthorized-error.dto.ts create mode 100644 apps/api-gateway/src/dtos/update-profile.dto.ts create mode 100644 apps/api-gateway/src/dtos/update-revocation-registry.dto.ts create mode 100644 apps/api-gateway/src/dtos/user-counts.dto.ts create mode 100644 apps/api-gateway/src/dtos/user-profile-update.dto.ts create mode 100644 apps/api-gateway/src/dtos/user-role-org-perms.dto.ts create mode 100644 apps/api-gateway/src/dtos/wallet-details.dto.ts create mode 100644 apps/api-gateway/src/enum.ts create mode 100644 apps/api-gateway/src/fido/dto/fido-user.dto.ts create mode 100644 apps/api-gateway/src/fido/fido.controller.ts create mode 100644 apps/api-gateway/src/fido/fido.module.ts create mode 100644 apps/api-gateway/src/fido/fido.service.ts create mode 100644 apps/api-gateway/src/interfaces/ISchemaSearch.interface.ts create mode 100644 apps/api-gateway/src/interfaces/ISocket.interface.ts create mode 100644 apps/api-gateway/src/interfaces/IUserRequestInterface.ts create mode 100644 apps/api-gateway/src/interfaces/fileExport.interface.ts create mode 100644 apps/api-gateway/src/issuance/dtos/issuance.dto.ts create mode 100644 apps/api-gateway/src/issuance/enums/Issuance.enum.ts create mode 100644 apps/api-gateway/src/issuance/interfaces/index.ts create mode 100644 apps/api-gateway/src/issuance/issuance.controller.ts create mode 100644 apps/api-gateway/src/issuance/issuance.module.ts create mode 100644 apps/api-gateway/src/issuance/issuance.service.ts create mode 100644 apps/api-gateway/src/main.ts create mode 100644 apps/api-gateway/src/organization/dtos/create-organization-dto.ts create mode 100644 apps/api-gateway/src/organization/dtos/get-all-organizations.dto.ts create mode 100644 apps/api-gateway/src/organization/dtos/get-all-sent-invitations.dto.ts create mode 100644 apps/api-gateway/src/organization/dtos/send-invitation.dto.ts create mode 100644 apps/api-gateway/src/organization/dtos/update-organization-dto.ts create mode 100644 apps/api-gateway/src/organization/dtos/update-user-roles.dto.ts create mode 100644 apps/api-gateway/src/organization/organization.controller.ts create mode 100644 apps/api-gateway/src/organization/organization.module.ts create mode 100644 apps/api-gateway/src/organization/organization.service.ts create mode 100644 apps/api-gateway/src/platform/platform.controller.spec.ts create mode 100644 apps/api-gateway/src/platform/platform.controller.ts create mode 100644 apps/api-gateway/src/platform/platform.interface.ts create mode 100644 apps/api-gateway/src/platform/platform.module.ts create mode 100644 apps/api-gateway/src/platform/platform.service.ts create mode 100644 apps/api-gateway/src/revocation/revocation.controller.ts create mode 100644 apps/api-gateway/src/revocation/revocation.module.ts create mode 100644 apps/api-gateway/src/revocation/revocation.service.ts create mode 100644 apps/api-gateway/src/schema/dtos/create-schema.dto.ts create mode 100644 apps/api-gateway/src/schema/dtos/get-all-schema.dto.ts create mode 100644 apps/api-gateway/src/schema/interfaces/index.ts create mode 100644 apps/api-gateway/src/schema/schema.controller.spec.ts create mode 100644 apps/api-gateway/src/schema/schema.controller.ts create mode 100644 apps/api-gateway/src/schema/schema.module.ts create mode 100644 apps/api-gateway/src/schema/schema.service.ts create mode 100644 apps/api-gateway/src/secrets/13b0e1df87be249a.pem create mode 100644 apps/api-gateway/src/secrets/idswallet.key create mode 100644 apps/api-gateway/src/user/dto/accept-reject-invitation.dto.ts create mode 100644 apps/api-gateway/src/user/dto/add-user.dto.ts create mode 100644 apps/api-gateway/src/user/dto/create-user.dto.ts create mode 100644 apps/api-gateway/src/user/dto/email-verify.dto.ts create mode 100644 apps/api-gateway/src/user/dto/get-all-invitations.dto.ts create mode 100644 apps/api-gateway/src/user/dto/get-all-users.dto.ts create mode 100644 apps/api-gateway/src/user/dto/login-user.dto.ts create mode 100644 apps/api-gateway/src/user/interfaces/index.ts create mode 100644 apps/api-gateway/src/user/user.controller.ts create mode 100644 apps/api-gateway/src/user/user.module.ts create mode 100644 apps/api-gateway/src/user/user.service.ts create mode 100644 apps/api-gateway/src/verification/dto/request-proof.dto.ts create mode 100644 apps/api-gateway/src/verification/dto/webhook-proof.dto.ts create mode 100644 apps/api-gateway/src/verification/interfaces/verification.interface.ts create mode 100644 apps/api-gateway/src/verification/verification.controller.ts create mode 100644 apps/api-gateway/src/verification/verification.module.ts create mode 100644 apps/api-gateway/src/verification/verification.service.ts create mode 100644 apps/api-gateway/test/app.e2e-spec.ts create mode 100644 apps/api-gateway/test/jest-e2e.json create mode 100644 apps/api-gateway/tsconfig.app.json create mode 100644 apps/connection/Dockerfile create mode 100644 apps/connection/src/connection.controller.ts create mode 100644 apps/connection/src/connection.module.ts create mode 100644 apps/connection/src/connection.repository.ts create mode 100644 apps/connection/src/connection.service.ts create mode 100644 apps/connection/src/enum.ts create mode 100644 apps/connection/src/interfaces/connection.interfaces.ts create mode 100644 apps/connection/src/main.ts create mode 100644 apps/connection/test/app.e2e-spec.ts create mode 100644 apps/connection/test/jest-e2e.json create mode 100644 apps/connection/tsconfig.app.json create mode 100644 apps/issuance/Dockerfile create mode 100644 apps/issuance/interfaces/issuance.interfaces.ts create mode 100644 apps/issuance/src/issuance.controller.ts create mode 100644 apps/issuance/src/issuance.module.ts create mode 100644 apps/issuance/src/issuance.repository.ts create mode 100644 apps/issuance/src/issuance.service.ts create mode 100644 apps/issuance/src/main.ts create mode 100644 apps/issuance/tsconfig.app.json create mode 100644 apps/ledger/Dockerfile create mode 100644 apps/ledger/src/credential-definition/credential-definition.controller.ts create mode 100644 apps/ledger/src/credential-definition/credential-definition.module.ts create mode 100644 apps/ledger/src/credential-definition/credential-definition.service.ts create mode 100644 apps/ledger/src/credential-definition/interfaces/create-credential-definition.interface.ts create mode 100644 apps/ledger/src/credential-definition/interfaces/credential-definition.interface.ts create mode 100644 apps/ledger/src/credential-definition/interfaces/index.ts create mode 100644 apps/ledger/src/credential-definition/repositories/credential-definition.repository.ts create mode 100644 apps/ledger/src/ledger.controller.spec.ts create mode 100644 apps/ledger/src/ledger.controller.ts create mode 100644 apps/ledger/src/ledger.module.ts create mode 100644 apps/ledger/src/ledger.service.ts create mode 100644 apps/ledger/src/main.ts create mode 100644 apps/ledger/src/schema/dtos/user-role-org-perms.dto.ts create mode 100644 apps/ledger/src/schema/interfaces/schema-payload.interface.ts create mode 100644 apps/ledger/src/schema/interfaces/schema.interface.ts create mode 100644 apps/ledger/src/schema/repositories/schema.repository.ts create mode 100644 apps/ledger/src/schema/schema.controller.ts create mode 100644 apps/ledger/src/schema/schema.interface.ts create mode 100644 apps/ledger/src/schema/schema.module.ts create mode 100644 apps/ledger/src/schema/schema.service.ts create mode 100644 apps/ledger/tsconfig.app.json create mode 100644 apps/organization/Dockerfile create mode 100644 apps/organization/dtos/create-organization.dto.ts create mode 100644 apps/organization/dtos/send-invitation.dto.ts create mode 100644 apps/organization/dtos/update-invitation.dt.ts create mode 100644 apps/organization/dtos/verify-email-token.dto.ts create mode 100644 apps/organization/interfaces/organization.interface.ts create mode 100644 apps/organization/repositories/organization.repository.ts create mode 100644 apps/organization/src/main.ts create mode 100644 apps/organization/src/organization.controller.ts create mode 100644 apps/organization/src/organization.module.ts create mode 100644 apps/organization/src/organization.service.ts create mode 100644 apps/organization/templates/organization-invitation.template.ts create mode 100644 apps/organization/templates/organization-onboard.template.ts create mode 100644 apps/organization/templates/organization-url-template.ts create mode 100644 apps/organization/tsconfig.app.json create mode 100644 apps/user/Dockerfile create mode 100644 apps/user/dtos/accept-reject-invitation.dto.ts create mode 100644 apps/user/dtos/create-user.dto.ts create mode 100644 apps/user/dtos/keycloak-register.dto.ts create mode 100644 apps/user/dtos/login-user.dto.ts create mode 100644 apps/user/dtos/verify-email.dto.ts create mode 100644 apps/user/interfaces/user.interface.ts create mode 100644 apps/user/repositories/fido-user.repository.ts create mode 100644 apps/user/repositories/user-device.repository.ts create mode 100644 apps/user/repositories/user.repository.ts create mode 100644 apps/user/src/fido/dtos/fido-user.dto.ts create mode 100644 apps/user/src/fido/fido.controller.ts create mode 100644 apps/user/src/fido/fido.module.ts create mode 100644 apps/user/src/fido/fido.service.ts create mode 100644 apps/user/src/main.ts create mode 100644 apps/user/src/user.controller.ts create mode 100644 apps/user/src/user.module.ts create mode 100644 apps/user/src/user.service.ts create mode 100644 apps/user/templates/user-onboard.template.ts create mode 100644 apps/user/templates/user-url-template.ts create mode 100644 apps/user/test/app.e2e-spec.ts create mode 100644 apps/user/test/jest-e2e.json create mode 100644 apps/user/tsconfig.app.json create mode 100644 apps/verification/Dockerfile create mode 100644 apps/verification/src/interfaces/verification.interface.ts create mode 100644 apps/verification/src/main.ts create mode 100644 apps/verification/src/repositories/verification.repository.ts create mode 100644 apps/verification/src/verification.controller.ts create mode 100644 apps/verification/src/verification.module.ts create mode 100644 apps/verification/src/verification.service.ts create mode 100644 apps/verification/tsconfig.app.json create mode 100644 docker-compose.yml create mode 100644 libs/client-registration/src/client-registration.module.ts create mode 100644 libs/client-registration/src/client-registration.service.spec.ts create mode 100644 libs/client-registration/src/client-registration.service.ts create mode 100644 libs/client-registration/src/dtos/accessTokenPayloadDto.ts create mode 100644 libs/client-registration/src/dtos/client-credential-token-payload.dto.ts create mode 100644 libs/client-registration/src/dtos/create-user.dto.ts create mode 100644 libs/client-registration/src/dtos/userTokenPayloadDto.ts create mode 100644 libs/client-registration/src/index.ts create mode 100644 libs/client-registration/tsconfig.lib.json create mode 100644 libs/common/src/cast.helper.ts create mode 100644 libs/common/src/common.constant.ts create mode 100644 libs/common/src/common.module.ts create mode 100644 libs/common/src/common.service.spec.ts create mode 100644 libs/common/src/common.service.ts create mode 100644 libs/common/src/common.utils.ts create mode 100644 libs/common/src/dtos/email.dto.ts create mode 100644 libs/common/src/exception-handler.ts create mode 100644 libs/common/src/index.ts create mode 100644 libs/common/src/interfaces/interface.ts create mode 100644 libs/common/src/interfaces/response.interface.ts create mode 100644 libs/common/src/response-messages/index.ts create mode 100644 libs/common/src/send-grid-helper-file.ts create mode 100644 libs/common/tsconfig.lib.json create mode 100644 libs/entities/base.number.entity.ts create mode 100644 libs/entities/base.system.entity.ts create mode 100644 libs/enum/src/enum.module.ts create mode 100644 libs/enum/src/enum.service.spec.ts create mode 100644 libs/enum/src/enum.service.ts create mode 100644 libs/enum/src/enum.ts create mode 100644 libs/enum/src/index.ts create mode 100644 libs/enum/tsconfig.lib.json create mode 100644 libs/http-exception.filter.ts create mode 100644 libs/keycloak-url/src/index.ts create mode 100644 libs/keycloak-url/src/keycloak-url.module.ts create mode 100644 libs/keycloak-url/src/keycloak-url.service.ts create mode 100644 libs/keycloak-url/tsconfig.lib.json create mode 100644 libs/nest-cli.json create mode 100644 libs/org-roles/enums/index.ts create mode 100644 libs/org-roles/repositories/index.ts create mode 100644 libs/org-roles/src/index.ts create mode 100644 libs/org-roles/src/org-roles.module.ts create mode 100644 libs/org-roles/src/org-roles.service.ts create mode 100644 libs/org-roles/tsconfig.lib.json create mode 100644 libs/prisma-service/prisma/schema.prisma create mode 100644 libs/prisma-service/prisma/seed.ts create mode 100644 libs/prisma-service/src/index.ts create mode 100644 libs/prisma-service/src/prisma-service.module.ts create mode 100644 libs/prisma-service/src/prisma-service.service.ts create mode 100644 libs/prisma-service/tsconfig.lib.json create mode 100644 libs/push-notifications/src/index.ts create mode 100644 libs/push-notifications/src/push-notifications.module.ts create mode 100644 libs/push-notifications/src/push-notifications.service.spec.ts create mode 100644 libs/push-notifications/src/push-notifications.service.ts create mode 100644 libs/push-notifications/tsconfig.lib.json create mode 100644 libs/response/src/index.ts create mode 100644 libs/response/src/response.module.ts create mode 100644 libs/response/src/response.service.spec.ts create mode 100644 libs/response/src/response.service.ts create mode 100644 libs/response/tsconfig.lib.json create mode 100644 libs/service/base.service.ts create mode 100644 libs/service/nats.options.ts create mode 100644 libs/user-org-roles/repositories/index.ts create mode 100644 libs/user-org-roles/src/index.ts create mode 100644 libs/user-org-roles/src/user-org-roles.module.ts create mode 100644 libs/user-org-roles/src/user-org-roles.service.ts create mode 100644 libs/user-org-roles/tsconfig.lib.json create mode 100644 libs/user-request/src/index.ts create mode 100644 libs/user-request/src/user-request.interface.ts create mode 100644 libs/user-request/src/user-request.module.ts create mode 100644 libs/user-request/src/user-request.service.spec.ts create mode 100644 libs/user-request/src/user-request.service.ts create mode 100644 libs/user-request/tsconfig.lib.json create mode 100644 nest-cli.json create mode 100755 package.json create mode 100644 resources/Blcokster_logo.png create mode 100644 resources/CREDEBLEmail.png create mode 100644 resources/CREDEBL_Logo.png create mode 100644 resources/Credebl_logo_with_slogan.svg create mode 100644 resources/Seropasspng-01.png create mode 100644 resources/check-mark-old.png create mode 100644 resources/check-mark.png create mode 100644 resources/checked.svg create mode 100644 resources/credebl-old.jpg create mode 100644 resources/credebl.jpg create mode 100644 resources/credebl.png create mode 100644 resources/deal-old.png create mode 100644 resources/deal.png create mode 100644 resources/invite.png create mode 100644 resources/lock-old.png create mode 100644 resources/lock.png create mode 100644 resources/mobie-otp.svg create mode 100644 resources/mobile-otp.png create mode 100644 resources/otp-image.svg create mode 100644 resources/platform-admn-logo.png create mode 100644 resources/qr_code.svg create mode 100644 resources/register-image.png create mode 100644 resources/reset-password.svg create mode 100644 resources/seropass.png create mode 100644 resources/shield.png create mode 100644 resources/verification-image.png create mode 100644 resources/verification-image.svg create mode 100644 resources/verify-email.svg create mode 100644 resources/welcome.svg create mode 100644 resources/welcomeotp.png create mode 100644 tsconfig.build.json create mode 100644 tsconfig.json 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 0000000000000000000000000000000000000000..0785c86cb18c54012b5e1915477218fdd4ad78b2 GIT binary patch literal 5766 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 literal 0 HcmV?d00001 diff --git a/resources/CREDEBL_Logo.png b/resources/CREDEBL_Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..db7628678865b00640d386dd6967130622a359ec GIT binary patch literal 3050 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 sAnkoJj7`0gw3U0y-3d*=_aHL<4=C!FeA+kx8vp + + + + + + + + diff --git a/resources/Seropasspng-01.png b/resources/Seropasspng-01.png new file mode 100644 index 0000000000000000000000000000000000000000..a47cfef67088566ceb6062954d1304694fe27408 GIT binary patch literal 7174 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 literal 0 HcmV?d00001 diff --git a/resources/check-mark.png b/resources/check-mark.png new file mode 100644 index 0000000000000000000000000000000000000000..d5beb4964ccfd6e290f5e9013a58a3011113bce0 GIT binary patch literal 901 zcmeAS@N?(olHy`uVBq!ia0vp^N+8U^3=*07`vZ_-EDmyaV!U}$ryj`REbxddW?F~6zfW#aSSn!z6ab&pucy}0jBy=okI>K|PW z;1J(`-=z811)G#}3hm|gSzT${`0prZ#{k2Bm8XkiNX49~A=k?e8Hnu8VRCU0N{dL$ zw|c=~)p&_@%DZp>>zB-6;B1_?tnTDi<<}=ZP3`<=suMQ*?!%IuvSqWcZW2CNxyMc} z=Y!q)_S_+*IH;w6(fB~Khnu5!@RUl^b>^*Z;{ zpp6Wg^Dis>9ECzgeeH4>F@95WsH0x+pc@1eCFh2 zUQk=oGdVa+m1TbKiF4ha%u6n_-jBX9O{CI#B6CpI4kfhn}y@&rEmicILEM-^HnU;r2Mar8BmVQ-Pd^_{|@k4JS zi`AI6`<;H#>i)g7;pXgR-d7jR?Wmr=z0=^LMVR`fpNDEsNcfd@EY+_n-+6vLw{6nP zcgJM6?u)iM@~Y40j!B@ypRMtm0=F7nELE$z?!x5rv}37Zw#VWfGu@`WT%mZGF?rd` h_tO&(ZuwpNlhJIVarQ*fJ3^p@;_2$=vd$@?2>`kR6L + + + + + + + + diff --git a/resources/credebl-old.jpg b/resources/credebl-old.jpg new file mode 100644 index 0000000000000000000000000000000000000000..39a554e366894af00b6bfb2c6e9446bbfce7f130 GIT binary patch literal 10549 zcmeHtXH-*bx9)<_TR=fNC`Awylp?(dQr&TsG%qwA|N7)AWc9@ln&B6 zDAEbN1*rlFHHMJfwf8yayWiR0{c-OZ{nX^!KejY&qejYx)^CD7W z=Y=i{^YMu*T)KSqnw*@RpqP@HqKvAPteniB522u^r)Oeh;$mjzlDWWlLFWJYAh!Z+ zbie>50HF{9DA_0=Y!u{9Ko9^Zs6lK0H2B{S3Q7nSH4QBtJp&_nLp3WvNdbXSQbDMx zsi?raL%`1gDmLmf7p~o)Icw@jE9A{C6P}zyC#?0Qjl*mhD}I{i(?+uXeTf{%qo#h)rFtEy{i>*^cYJ370%dwTo&M@GlSC(x5q(@V>km7l9? z>l>T6z5Rp3qhtJU!k@S(K%D<^{2Q>p;bH@EQBqMssA&JhMM3Eg4hS0+^@VFRXKt9% zI(nZKk_o3{*GkU$(nc>VYlh{x|8$t)oQT|#DDF>ae#FLK;I=!B9&{SWGTZX&Z~Y!^EZobD6X!D3 zVF&)Z$4H>}?LlwmQmEWszUlePgTn521oE>YUN6JT+zaY5CKPkccD9Etm{QzY8@(~{ z$g^|RO<(0~9vGd}5dz+jE*>9k+jBjUE9w8{#8Z7oe$^BHbvgf7=U1b;4IPC9`a$p> z^ytP(9!f3r4eB~&(6Gxws!aMaUS)*Dk^6Tm>YW_O_$bo;;^7w>wjOuu=bI zQt*$LlCzB}OZ;LTSE(z)Z5GP~R*fGZdOjZso(3K|J47)*HMdIcMC-GiTg^lvIa&-eoS(*fzFR}n=sPW zM1NPl-cr2Id4J95E8G_U>M4?hRTU^K>OE3LSL0QdnUS&HZax*GW{^`;&)4R&AA^o( z&;oS>kMLiY+_Q3193IELt_}0Jek$~8t@aqY(eotLK|z(%#&=6w#(?_!mHt|$=TFt6 z>ogMm6^FXJw+#4>i*IawQZF6g=q0M4r4APu^t^*_nXo6)zr?LlL_pD*LCN+Wh%|U= z{PRrf1#Tb7oH0`!gBv>Rw<-Rc2Bq1k8?>uo9eW(LQ_X~wlj^;(vljl_mjE%o=Hn`E z=4a*nF4zL+U0<&*Sn={VYmG;S2@BCNG#-tD9S5v53NN{3bGR-~tM$1H@GL*^il1be zNum?f3bK@l-Sub>+c#0c)H^@Y$Pgg|$^_YVb^iDcWvX+A?6X%Fyw8&q$N3R(wX%-@BJnS?sGHMX8R+o_%L8(ipC_%q9hKp=#D?c`^cTm%v-zkYOrg+MG62~BQNuYc`u(Fp)w@WKa=m#Z z9=Z-cpsemJf9!*R6|>vFPa52^BcU)?Ali*5*K>+R%)?W8@|5fd+qC>hwNJ>TMw=|o z;lY$P)Eck*&?06W`T5j{Um}Q%>k`=&X@ZkQB9fL<=nVEYAa`&+6b3V6jDo-Mv zb7u)j9k;8-wuQe|fV?Qet*4!gC$Oytl+HP~+X%EMn-t#_sVJHjVdT+>r z(Dr1VPX4eMND9s*j5m*2$QtoCo-w){&u3cf@Bc(1d@Lo9B68qibLp_;BAJ4>>^5Bm)aaLP=qEa*E$egHRZ&s<_jk3&E+9{amGu!qmSJiYacipA zFfb^-ed^<|FV~tccM8pKQDpe>%l(9x3QQ3`5pH=ODB~o*H)`Wr+MD^%9#yeRW981( z2q~+Y7Ywxgy4GVUP}(@)U|O<=3@qg}ULXUSb3P=ZHW}!7Oa`J7u~CQ>{_@bPI)7e( zOp2H5wP&*tuVK!yZUn}hSo0?4Z#t!8X`?o>UuF7t`Hq3vB*3p4H`A_s5#?K`8kjgU zqwf^po~tde=+%18%NWYt&7wGH103>5_9B+@R0YX^I?uu>KAsFDnUVo!?nN^28#_S; zb~|Cjr(tS=aeIRJ$Ah$FU@)qd41@}S?3lKIH|c7CB^>CCKTChvw#qdpYJ}oV;Jez# z-9{S`7zZrm#_g4vU=|_|1`Lhb8E9cc;QF5-s!Sr^w^x7s!bRG@!7Z5oe2BK(^sWF84jE0fh1|;B^qNgu)vvc^ z4Xn7%#S)02sa*%a$=>T>b|Vi?b(e>9KG_Gzu2})vTbBw zf2oWNEX1_ne+puUv!G-ko7s~@{L{7K5Ofg=bbeF<7D2?jA&!@&NlFf}OUSb>^H+wJ z#;c00w^u|B>pJpSK6Qvug{tYf0@L8cK`qmaZ4>D!Gx#ydoJsD>}WOi7|iaRj7zjYgLnp zi&HvtcO3KZnPZ(yW)yARQSoEYL*joQ6m`pu2+YMIhkcU~)1)zT@AI?~CjBihnaMzi z)#nqd9|%^I`V8j1QMX^u`55P#R?gp%_P^cLUC|*dPb|tHCQFtH^3oh3wQ^!yN6+gF^V^M)CSS7l zmTEc}xcQp^ZpCAiI3fWJn-=LQ&_241aZ=i+BOFj2c58GuslZ0Mt3S-U%pCE?#L*n6 z-5@-$8n8*q_APKdJ2`)I*iNg<+GO?G=C#NkDpoz41?e2!veS>N@VBWi>P-rs4H$mw zRMxVeQF!O>^_;^a?~5gWVgS3T)H{!4hf5EKcf>|f#Lxb`E*0mD-<+3K;`4Wqlx%I5 zeb+u69{0#au5adem<)Iw%r`H3_n7Wb2O?usRip}iIAem<v+cz9X__`Zp^-|Uu>kPO^<6;OMHG_{>PCXPXQ2JUoC+;uf% zUC2q~Qm*`}%y>)xvitq&n%Mghq6{&46q+UMX3tPr<6kRB3}+Z&vcXUBk-neYeDId- z?r$HyqpqLqITbRE&_+~5L--Xa1KKegZ}Hl;^{ccb|vL@T1 zAGu_G@C?UuoW@A~;mgm~y?;)(zAUG`{fP!)*rF-O0x5?JcCN)9%sH8)Q+cOp^reL# zTbj@E5Xe9)=~8^R1KXF0ygLS>_NkqPLP{1((LM=#vOnlhY^Qn82E{txS{C#1S}srJ z1W;nXCzz-gGHC#6l4Ymw&)|YNi++Epu#v|-zMJ5o!mh4@c|r79e+-#&Th@rt*fVW3 zAOnsn$WyNIqo2WgI9igSLafFwF>f-E#}lS9C%i~gS-M`MoN3dY&FBAmcveWc`6o+G zn5yq;nZbzyKGN~pmW`9by_;czJuaj>JM%PTAO*41azZ^#2G$G82ms#m?udLgH9TQz zKL52%THfUtTwm`_d4h(PvY-|ns#Ki|-eAH5zc^!7*VYu9SUr5FRYr_0R-QIQQfM8Sn|1BJ7q&AGupcg_q0Hu<{nQ3v|`IpFD1ZjfAOnH?yJ< zEBD4)wl-m>Oz62oY!p~*2-6gRn#l~b=uw!O@!(Ma8ui3|2Qwizl&V`H?s!r29^K%# zU2{Z-0z8@&Y2*A;j0~KGX+0L)lX5{Eu5psWBn`vVT|j#Fz(G%Gss{T_NEe$@mwFU1gv!TR`aj*#`@o>27DDUM9|uS-#xLN>Kphz0Kkp&^O9t_dp>KTU3s4HaXkH*wkE5V7yt+wwrv5ZSqWA-*M|P$H5#s z1$I_;E0i90{Xo`*Q2o5lF_S~B?N`5^#|_##`6IDNRRh8JVBx|o?;>q)!CfcFj(&bg z_;wv%U;@wY-J_V=C}>R0-pz1_5M?D%Vf?$`_|)BMi%^aYSmig_V8zU{FB=j*KyOsY zo*Dsxps=)F8N~DFD_9OmoYE1ZJ1)SJ^}(LF~Xhn72Mm zWc&tqQYU(3>^ALy#@{Yh5!JEx;hgQs8ZTh|24(9z4nnm&c3WnA#iF6)YZBD=| zCuK9pv5C#^t*8>X?T|xpz)r~$vp-3f{}!qFCm9$T2w8y9xwY(F-Gm(;OOv7=L$(K5 z$UtvpJ{dq~Y7l@j>v1WquqE6>2Vi%5jB;i#rWN%Y zXGl8oBZ)y@c7GMN+%pdG86)ar#m92eZAOL3z#p+bs<+6%l~?`(rL-Q4frq$20+(Ty zOk&2Wp-hfS-saJ>n^K(;VFpgE9Qz8f)luk1y`Z8mDi2fHBCb`_GF;nAa9+6Bkf_iz z(zD2{ty7Q`_R6k&LX(MS53JYRt>eP--aeU#o5^ zbDrYA3xC?`%Wi5HwFIMrfh(xHMf$3-FMr$=%!VKX7kE*GUu59>5bb>NG3WZFv9Xi* z1NKb-<>TfVL?5+Ew;GfADp?zXyzPSPPS*X>9nMC>VR29VKwvz4xrO(!17l5uyT^9U ze&1cEn^JphiIb4ey?u(r9}D?ztu=KfC7 z&9kT&(s?q_r;8IieZHe{;B=eTIiDDaQ%B}aZoG6MXm>b!yv9`}8{YjSZklN@7Cva~ z`aQeKB!2$-ozKI0_vu(Z_Z#BI9%;!g)Rm(R~U*ALc zf}20q34&ER(-l>gh7c0+D0;N;yBYVp00TW?F-U|g^4JLQ@>j4~NFeGy$|xN3wM8=k zc9of403V=613;=nJh5Bt!fg)BRU>ao?7!^qyw{_#geV>Yz=z|REXh7H;B4e$a%bc<9^4JjcOszV|P=a#P0-!@h^2?V7E6K-+v_ihOpSkyw`*eOuhl4VfCv&xmMHB}}} zAN|m=R*eX*wW2EK7Kz+@scV7?hH97lw5u}?H=g~dmV{*d)K7XaYB-ah*1+5Y2~z|B zTD3kgP>(g!HGy596C`1C;X8W%5UaCGM9d=4mZV2v>c?QI3|SSNRv{U(WRrn=AZrX) zpmRt+7WRy#5WhsFN%2XI`$tJ&mW?bV1Cy6D4xUu6BPwf&sqxt-^dPES%m=6~D!d&2 z^$o}!x+LG@0g%fnGiWF`meM(3NWtdeC zqC*ufOA6W~19;YKqK+VTP=JXHWD#0ORA6Rg)8t)X3=Wb;HfpHNjpzHJYbpcGN^kyp z5_TtDB)Nxct+0jvrw{2Y_I2uFli-Q0LF~juAK}Kt`W2_{y9evQB1_s3xR4axKl$x^ z&68oFR;?48D4#P55 z0l{;iwuB+o9o@lgtdZ=wovKdjzj1w1d7&_@cjI>@F1V!n3pdV9f>?yM?Zp@YBjW&XP*^teW(#> z^Ty3y@0K%=7ds?SDC`z`7xtscwmKNSIN(EKBLnO<4k~Hk@A@;>5)%{FnkhO$ISx`@ z_S3b|7C1aKXs=b!y?l9>5t=!thLN5YYz|W+gxOF!iRCKIEJb5wuK7S z;H;iM+cul`P0JFsZdJ{11vWd%eYVC}1KI33f>rYH@tp#!Vco+8r_1L|L$`SC& zO;XXfVOF!{Nf9=}U|A&{rYExa|ysb4kp%+2UxyV~oT(8IkosRqDbGCmCFbl9x{tN54SSJChEY84@vAv1q4c!v>ev{r!69W75x0oC3iL`K~s| zNL6dkz?h*0ms+v!v3=%gq7K)4GaXL4^WqXi${5B!k>)yS3F)?=6Vp>*-&bhClc!~QSrHrHlSCI;^;>>7?!Kx zO%C1&Fe*!yi<*V-{ah*Hjkk(&&og|g%oOKi3S~#Mf5V|pnF!gZWB~kD83)dac)5iK zEC>gS!IsZHOqX~LCxu!@LgXhZmFnsuwzo(8jLL+lwdh;hlRpWLH9Rxdm?>dYug9WSfJ%tJT~F+;Ke0eV!3{#ua(C z*a}vCl?jYd+GI_By&n6gqc-@2K4+oc*Vl_5?dm=pM40%h~H5l2J zGrBl>>of9ObybYc8O&#XW95_pvHHGUO&$)cd=D}78>s%Fe+h_^^?AmVn4elHBBiDI z_ll`j*rvnO4|AI($iRzqj3*f{dcAZJZs}+L@qwKq^8Qo9wdMu86pD3l=0alV4RB`8-{=K->?J5wKC9VH4-B>b z2WWEsrQgmJZ< z(cz4h7fF(5D5~TWhXBw5>Ho9|H3eOVVdtbfTX;%9nf^T6Oz|)?nSh=UpdLpZ4pO=B zWPN-mE#=hk zI;pZACioGGFdIpY6X~5Aw1{?tk0GCi^^Vd$r3wmenHQU^A)f|1%P(0TbK1D?nLrvj zud4ra?6^cbM7k-MgYr|Yn?8p)qk8n7A$>i>`Y><4Zh;!UIoic~rLoKXP!5)L2A;}q zEj#AlV@g0fJBS)SeO~{teu4XvGA>|!G1_IRVo1MCf<4tE(NxlYz1_NXmnqj#7Z;*5HHeh+_14cG5>WM%b_0LQVy z@9J^l<-#BSh?BS^qJQ5-#P{Znx=KE3fsc*pT?meH>fNbe)!R4NewW+JJW!8=@`en; z*2;L!mTpryo8{kY!l2j7&09mo-gPN6+Szp$dsBV9bRAM%N5!^*J~%EfugrW%ulVh0 zo~YTdYvb-q=CHv+FpNqZJAWmLH<_FWkQdqTD^H0=yzFZ*#Mz?!4#6+xM zIzlmyu~(M#a}&)(ZWqN|WoH3^X9DB$;O}bIR#|+*@{C=LO~X}N@2b1S-j4HkD+hO; z?Q$LdYuPzGpSa6QT2(h!M{LgZF0kXGb$5GJ1zg^SraKncz6-hDKmCJpCc$s`vRk&D zI7Ch9h{F%;eDEB%&kK(2@_=Yoe(rG+zS+^S2 zz`{7ehKnBYJn0M=>#&<-pe-0iH1ZtQv<-r7b$ZJw!WnNt(b>WkTfa0=pv~6xgL?5bjH;l<$k=a?oHQ2`MulV&qE`Q4c zx}N;2OXdFE$bo`A62Aigih%k0zjP}8>|W4h0n3eR zJuNIPHP(kl7dWJQzkhi^ojvQvOc0!C5h6+pV9fIHxnJpm~Z9=d{P|ZUwfS9 ztL9)PlbyNhUO>s?3YSBG@oZ#pHQqmNYX>pWT{=EE&(6w^ zlP}WhNe8!65V%!Gf+gb>RM6d)GIO<=1gmQ(_?8wH|14|!QN4ruAPxVXa^#fBSR8iy zR@XGCl-pC@Hd>eK@o>!r!ly-nIh}_yHnR>tKHId3?A7XH%)W<)F`a zGI;S*xl9_}i8AF2`mx|~hEP82X<^hs%)yDKKJBxlxd%>Tq_9wqNq_kALz zFF*R8t;(*Ts*=f<}^2@yZZJNQ5!-*$-us sd=Puupz^pxu*Yh0A=j^HQymWK7ypzz|2dKV&-?#eSo~M~AxNJ3Uvo>QVgLXD literal 0 HcmV?d00001 diff --git a/resources/credebl.jpg b/resources/credebl.jpg new file mode 100644 index 0000000000000000000000000000000000000000..39a554e366894af00b6bfb2c6e9446bbfce7f130 GIT binary patch literal 10549 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 literal 0 HcmV?d00001 diff --git a/resources/credebl.png b/resources/credebl.png new file mode 100644 index 0000000000000000000000000000000000000000..c85edc6c5a9eca18e5badda27a28d96d4d454d76 GIT binary patch literal 42296 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�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 F{{b(Ibq)Xk literal 0 HcmV?d00001 diff --git a/resources/deal-old.png b/resources/deal-old.png new file mode 100644 index 0000000000000000000000000000000000000000..93a7e11daa3c9fed6e5b2cd1a85650fb34ebd32d GIT binary patch literal 12991 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 z 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 JUUh!-e*uT zaNQ|dJkwCOrCj?e_l(YVL64P8I{oJ+u0L^^xiKgp=VlU{+R2J8W!9#exUw6wmGYiB zAGqkr?s(96N^A1;p437&nT~|~$x{pbSnLj&CDsb?avqI0@s^dA;JTooz9e+k++|i4 zuR@jw_wpY$vaiZLed>6F?>uWUy$_3D?z!}4#Wc;0F?)Sf7NiKr_8F8uydLfKX>m9( zjb8F}aSW-Llbo@g%o0&r6s;n84W=X9OGJY7veAQ}wm7w!%wh+~Ay#cDX zd?xvZ8eV%ESX7qbb!gE8FA3GzOQWKEB`!?evbFrc6a#}2v9d_R1P#uWrEkj^n&#c} zV%IRpW|~<0mrq#DZx&C`WRs)jQXI>qSlyDFGA|!|c0$@9<$Oa#3uD^0gHtxVUfFf) zd=@kF)0;C-vj<9)4wKg`Pyqv*~S45yh!TWuGZ(A50Rw;_mF{`TNYzskLQm txka8YZf-SD-S}*d#eqd4Jk2Lr7$&`#8=NFvy$_V>JYD@<);T3K0RYC?EKmRd literal 0 HcmV?d00001 diff --git a/resources/invite.png b/resources/invite.png new file mode 100644 index 0000000000000000000000000000000000000000..366cd1af97429f6c873dc0fe2e0cca3300355850 GIT binary patch literal 7822 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

xs~ETDpD9IDv7?ib#P8+*G~9jd3Oc2G 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 R!A(oZdDnhQ`OksB{0G_BQUd@0 literal 0 HcmV?d00001 diff --git a/resources/lock.png b/resources/lock.png new file mode 100644 index 0000000000000000000000000000000000000000..7c800f1542241bf7826e105cbdc368142c704f17 GIT binary patch literal 1114 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=3?wxlRx|@C#^NA%C&rs6b?Si}&H|6fVg?3o zVGw3ym^DWNsGujnC&U#fISK|z2v{$WiUT^Or6kBNm_a?hZSL~<-6=-Q0^#TW-^>)_ zy7%vSsUXknzmJaYJal)L@YP!x5<+25b@}%^$mQh>y`sppME1a0*5E%!Dj9FTH4(r6 zOgqlz+vE+krp}Ui1wYv@&6;|7#s0%H++Th8l6p~VUZftMkA?ZQ$G>ajx|DiuH0b=g zb9()GQA5oiW!qkxEnc-OVCRRF*}!yi-P6S}q+-t1tG~mA97Wha7%I6w(pC_AXw7ZR zq1fG^q+_zggICk+O`_o57mm}CBGbP1my54H{P^9wH9bpYT4G1_FWZc+MrC*2mhUtC=f04Xk&=_l{qv>w<^GI6 zJFMy-9XWYya{Z-+tAc~A9+fntv7B%Dr?TwuE2)W_XC*z8>FuAFfAZ(zDZ2$OD}7`s z*!Xu#nf6R?&$t#n=9M@5zD!;1P}QgI7nL{fWc+CcZkvU%-wU7mr8WK&Fjm-8GU@f3 zsKQm}c=Ht8e{b6OB#kTL3EQ-Wr_nku&oXP@abVnWmP_#+gR7o@X^6>**aHWju1pE! zb&@_&uHwy@D!OC&hlyT}50-+7r(osfz_VSDDP57r^cATl{sZ&dlZFd+Hu9S${>-`yP|&_KvHw zcDFEAy2q~Y-tppJRLaR;)ejCIVl(ZWq$4RRr#u4wXonw&h|B)6Xq6i|J?Qatj50o7i`zW a{bAY7vLJueyQnHq>i2Z@b6Mw<&;$TDK1|O5 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..c8b9347618b0b0a26fb974b7bf66499d438c43e0 GIT binary patch literal 12796 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..d0889f9b3e86eb78b5d795d44cf9ab7e95b9dfc8 GIT binary patch literal 2538 zcmV@I(WYO5(I9G31afY(K$99XHlccd*y%r|` z010wQL_t(|ob8?0wyQb}MYHK7q5S{Xo)DT1E-K5AamQUx`{A76i7m-RGLOIih#q%0 z0Hyyy;>zo~$vzs&kETDermpi;PfG#P)>&0PA0EBSVNAdS2Fd%ZEPM#2iZa8}073G; z@^*u#bA}%FbQH*IeDfeybz=)@00iVUFuUT=F$fktNG|OQQ)Qzy7%fO$=_$S{s@HuW zd7~OqN0rSzAbCb|v>ZL_-7u_Hy-D+X8c1^=#)D`mB#_}&j*kezkpgm)nVX86 zd>kb2?xvVjsX_AF+?A~uNetvyIHDvGkl*2mk%U2hi6cT12KgzDFi8;P2mG$BK?d@3 z9HGb<02%LYwM*V}2tP=FcblCmd^1Qd$er`uke;4dy$>W$zIP>6$piU`36@)K*X04Z zcDsZrr@^~H>bu(-RN>n2a)SJ>fy{E5i4z25AjQzvb^VvuBlp}oCdWF<1%inn_TfO= zKw-|&!~v2cB8Y1b`ZdB>-V_kTz#2SZOL-!{>omB|AOMdQ+`j>_96i?w1mLmC*xq0R zI4e4Ot`mqp6h=2ZCKUbaI)ud|NUOGV3dg78)ti2Y+2avJy_lkLd{k>+nQiX}5VfU~ zIrPVPh1Deo5UmxyIft>@JsRt!*&x~u@vh%IeWDy)rVF+~v>n3DzEHk6vr8Vp-+erp z9O?rLqf0ChZHMr4NR;zBqf1PX>pMgpe|O(vf@ot%U?0${T=&S`C=HNS$>CTe5Tu)H z7cLO(q@i#$GcM&h>t>b)h<3{0*>H+TIS#fJ)()aRf@aK4Oex>rI-YkB^%_Bzi>Q6> z`ua8ywW;3(vRdW_qCZ3sf@oKA{Q*S3WV;EZvq4^Jj=MmX=>9<`w^3&G3kby4g@GM1wz35Ta;SGFNV$L@e#>!Ud>zPp2EoL2 zVQD7DW6mI;6dGCeGv?v#7X*~kB&&VKJiJDN$l?g5JhSk-Vevo8p$W>qqc z2j#M2K{^2l=-K5L1eO@fH+NL-x(9VT~^0SF$IPsO0R;06!g6m zWDQpV2cd5_LOTQ<*&m+b4ws;{HZuseCW2@spl#YkC3HQqh<3VweQ1{g#8+Tv30$o? zuMp9ttc1Y@+DhA$A$L)BMJFooBCh@m0x!yqIgm7RemuW9khA zGZ{VJ)q8&DoxN6RK_lUB13?&x>&juPz}R&ed5iF0r2(RTHBaBo$*(4>%8$K!@9l5Y zW2O| zW@Ld_i^w$tW=|JmSemdWj$c?*eVCT9^g+f4sy=TFDqLi!l8DIN~Y(P4wcyW}EMpAiZ&<}Vys(XnN>

gqs25DLc7^@cAvNq&^$}cP zNsC>C3W6M#cIWby&{9)!sg6RSWyx_(bOs4I3;cFNP=WdQX)WO>C<@s7r6v+IlG7`7 zRYrN2XBV4Dz(~$vzy;zRTo;>&-(DSyRA;T4Z*W~2MJD+oiNjXiMR%UTrTeO+iHj0gWQ?f=4qUBZ%A6ist|o(_};Q>bf(^ybI4`wZcFtJ7spRrw#wb* zSXOvmrx(PLt(S1)+NZPs^u;p20iQ1#z1(RzV!JnmHQ-AHZ-BSs*JEg=ZOVV!nC8pc&kY4_gD#6Km*3upHkIi$U!}{?P)(7Av+ESYt z(>q?6TdP0bNvA98cW+ouk{PeJ1SEQ&bG4<=@n+lXiATLIb*dyNTSh%MX7OiGdu~1+ zWlNUlo1tVXxEB~`yv5@3&onX9nn&cGz~&N|{HL zuQH5}gLRE+;$o&P3o$exR<3bTNm+8U?4py$C*8Ww%4Y8zg)XuGKN?3$h01kk7qGeY zvBqcBklp`l?9AQew{Cc5`Up+k4ECwTb5JkYFk1@!sZ{D+4q0E@Lq4zT zCL7A*c?G6;1GYEP`#(JSEK6^y&RgFXvyhuS+%L7h#jegF^lnw#Xa3i3bW-OpgOe%A zwp(70nP)f^WtTN6Rx@e$59^P{?5lD5JK>}L0nPMl&Ay + + + + + + + + diff --git a/resources/register-image.png b/resources/register-image.png new file mode 100644 index 0000000000000000000000000000000000000000..edff2da341a675a7abc193ac413a860815b0bbd2 GIT binary patch literal 10356 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 JN*Fxk{{WB#PsacN literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..6c3c35167f104bc44cf77092a5ac6d3cbaa86831 GIT binary patch literal 4870 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 literal 0 HcmV?d00001 diff --git a/resources/shield.png b/resources/shield.png new file mode 100644 index 0000000000000000000000000000000000000000..5479acaed1f41a928319e3d3173c2fba30584419 GIT binary patch literal 1212 zcmeAS@N?(olHy`uVBq!ia0vp^N+8U^3=*07`vZ_-EDmyaV!U}$ryj`REbxddW?Oxi3ZL6-e9r!*O@9l#!8mYTkJP$t&lSA$k-t9CcBSW znm7ODR{?_;vp6Luu)dx8TzlHz_&Kc4`f1 zkvqP0$rP?VZkoOKZ|?{R+_^8d>$l<)Hk+CU6C=4TL;5br1Jl`0PZ!6KiaA>+zm67i z6gk%3l()~VBSONAEo<$g))?h8A~*HAx@L%HUG*^TG&*afd&E1l^8A(h8#hzKZ?MYA z+gI4Vnfdu#{<)dPo1IV0JX`Q^(Zv$Y|9_rphp!i9XuW>cyy#rCk82pi(QQKCw%*&5 zdQ$1l($=Fn>z-^|{Dv_HE6MyZvRd zhZGe&{=GD|tog?zJNFDXV}T>V0j{LO!{i=+rR3udw(SZ(J0 zGP~pOCK<^?3##IjC8i4P>ulO5`?;BW$&&{fZpTEQ$R79i*mvo+yXKDXlQk=w`fCM5 zjwCwA$obT?XOucF{F9y$XDo2K%J#!? zofPGkp3UFI{_ftm^JrXTx?aQ0Z#VcFVdQ&MBb@0H8!sW&i*H literal 0 HcmV?d00001 diff --git a/resources/verification-image.png b/resources/verification-image.png new file mode 100644 index 0000000000000000000000000000000000000000..882a307b1e8172ec1a9f327674a3391e0b092025 GIT binary patch literal 16583 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 w-iy^cTs`LzzP!=z|A_5wj~KMReTQCl{ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 0000000000000000000000000000000000000000..56cf05ad231722f9f6ad64be95c159753481161e GIT binary patch literal 13247 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 literal 0 HcmV?d00001 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