diff --git a/.env.sample b/.env.sample index 1739e22e2..9e9e35b11 100644 --- a/.env.sample +++ b/.env.sample @@ -37,7 +37,7 @@ WALLET_STORAGE_PASSWORD=xxxxxx CRYPTO_PRIVATE_KEY=xxxxx-xxxxx-xxxxx-xxxxx #It should be same as studio UI -AFJ_VERSION=afj-0.4.0:latest +AFJ_VERSION=afj-0.4.1:latest FIDO_API_ENDPOINT=http://localhost:8000 # Host:port of your FIDO (WebAuthn) Server diff --git a/Dockerfiles/Dockerfile.ecosystem b/Dockerfiles/Dockerfile.ecosystem new file mode 100644 index 000000000..3f258801a --- /dev/null +++ b/Dockerfiles/Dockerfile.ecosystem @@ -0,0 +1,41 @@ +# Stage 1: Build the application +FROM node:18-alpine as build +RUN npm install -g pnpm +# Set the working directory +WORKDIR /app + +# Copy package.json and package-lock.json +COPY package.json ./ +#COPY package-lock.json ./ + +# Install dependencies +RUN pnpm i + +# Copy the rest of the application code +COPY . . +RUN cd libs/prisma-service && npx prisma migrate deploy && npx prisma generate + +# Build the issuance service +RUN pnpm run build ecosystem + +# Stage 2: Create the final image +FROM node:18-alpine +RUN npm install -g pnpm +# Set the working directory +WORKDIR /app + +# Copy the compiled code from the build stage +COPY --from=build /app/dist/apps/ecosystem/ ./dist/apps/ecosystem/ + +# Copy the libs folder from the build stage +COPY --from=build /app/libs/ ./libs/ +#COPY --from=build /app/package.json ./ +COPY --from=build /app/node_modules ./node_modules + + +# Set the command to run the microservice +CMD ["sh", "-c", "cd libs/prisma-service && npx prisma migrate deploy && npx prisma generate && cd ../.. && node dist/apps/ecosystem/main.js"] + +# docker build -t ecosystem -f Dockerfiles/Dockerfile.ecosystem . +# docker run -d --env-file .env --name ecosystem docker.io/library/ecosystem +# docker logs -f ecosystem diff --git a/apps/agent-provisioning/AFJ/scripts/start_agent.sh b/apps/agent-provisioning/AFJ/scripts/start_agent.sh index 881ed70d6..cbe95d936 100755 --- a/apps/agent-provisioning/AFJ/scripts/start_agent.sh +++ b/apps/agent-provisioning/AFJ/scripts/start_agent.sh @@ -14,6 +14,7 @@ CONTAINER_NAME=${11} PROTOCOL=${12} TENANT=${13} AFJ_VERSION=${14} +INDY_LEDGER=${15} ADMIN_PORT=$((8000 + $AGENCY)) INBOUND_PORT=$((9000 + $AGENCY)) CONTROLLER_PORT=$((3000 + $AGENCY)) @@ -48,13 +49,14 @@ cat <>${PWD}/apps/agent-provisioning/AFJ/agent-config/${AGENCY}_${CONTAINE "label": "${AGENCY}_${CONTAINER_NAME}", "walletId": "$WALLET_NAME", "walletKey": "$WALLET_PASSWORD", - "walletType": "postgres_storage", + "walletType": "postgres", "walletUrl": "$WALLET_STORAGE_HOST:$WALLET_STORAGE_PORT", "walletAccount": "$WALLET_STORAGE_USER", "walletPassword": "$WALLET_STORAGE_PASSWORD", "walletAdminAccount": "$WALLET_STORAGE_USER", "walletAdminPassword": "$WALLET_STORAGE_PASSWORD", "walletScheme": "DatabasePerWallet", + "indyLedger": $INDY_LEDGER, "endpoint": [ "$AGENT_ENDPOINT" ], diff --git a/apps/agent-provisioning/AFJ/scripts/start_agent_ecs.sh b/apps/agent-provisioning/AFJ/scripts/start_agent_ecs.sh index f757e53bb..1233a5de2 100644 --- a/apps/agent-provisioning/AFJ/scripts/start_agent_ecs.sh +++ b/apps/agent-provisioning/AFJ/scripts/start_agent_ecs.sh @@ -16,9 +16,10 @@ CONTAINER_NAME=${11} PROTOCOL=${12} TENANT=${13} AFJ_VERSION=${14} -AGENT_HOST=${15} -AWS_ACCOUNT_ID=${16} -S3_BUCKET_ARN=${17} +INDY_LEDGER=${15} +AGENT_HOST=${16} +AWS_ACCOUNT_ID=${17} +S3_BUCKET_ARN=${18} ADMIN_PORT=$((8000 + AGENCY)) INBOUND_PORT=$((9000 + AGENCY)) CONTROLLER_PORT=$((3000 + AGENCY)) @@ -38,13 +39,14 @@ cat <>/app/agent-provisioning/AFJ/agent-config/${AGENCY}_${CONTAINER_NAME} "label": "${AGENCY}_${CONTAINER_NAME}", "walletId": "$WALLET_NAME", "walletKey": "$WALLET_PASSWORD", - "walletType": "postgres_storage", + "walletType": "postgres", "walletUrl": "$WALLET_STORAGE_HOST:$WALLET_STORAGE_PORT", "walletAccount": "$WALLET_STORAGE_USER", "walletPassword": "$WALLET_STORAGE_PASSWORD", "walletAdminAccount": "$WALLET_STORAGE_USER", "walletAdminPassword": "$WALLET_STORAGE_PASSWORD", "walletScheme": "DatabasePerWallet", + "indyLedger": $INDY_LEDGER, "endpoint": [ "$AGENT_ENDPOINT" ], diff --git a/apps/agent-provisioning/src/agent-provisioning.service.ts b/apps/agent-provisioning/src/agent-provisioning.service.ts index 596f3ae93..3b85d3e2e 100644 --- a/apps/agent-provisioning/src/agent-provisioning.service.ts +++ b/apps/agent-provisioning/src/agent-provisioning.service.ts @@ -22,12 +22,12 @@ export class AgentProvisioningService { async walletProvision(payload: IWalletProvision): Promise { try { - const { containerName, externalIp, orgId, seed, walletName, walletPassword, walletStorageHost, walletStoragePassword, walletStoragePort, walletStorageUser, webhookEndpoint, agentType, protocol, afjVersion, tenant } = payload; + const { containerName, externalIp, orgId, seed, walletName, walletPassword, walletStorageHost, walletStoragePassword, walletStoragePort, walletStorageUser, webhookEndpoint, agentType, protocol, afjVersion, tenant, indyLedger } = 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}`; + } ${orgId} "${externalIp}" "${walletName}" "${walletPassword}" ${seed} ${webhookEndpoint} ${walletStorageHost} ${walletStoragePort} ${walletStorageUser} ${walletStoragePassword} ${containerName} ${protocol} ${tenant} ${afjVersion} ${indyLedger} ${process.env.AGENT_HOST} ${process.env.AWS_ACCOUNT_ID} ${process.env.S3_BUCKET_ARN}`; const spinUpResponse: object = new Promise(async (resolve) => { diff --git a/apps/agent-provisioning/src/interface/agent-provisioning.interfaces.ts b/apps/agent-provisioning/src/interface/agent-provisioning.interfaces.ts index d74d14090..0d5042eda 100644 --- a/apps/agent-provisioning/src/interface/agent-provisioning.interfaces.ts +++ b/apps/agent-provisioning/src/interface/agent-provisioning.interfaces.ts @@ -15,7 +15,7 @@ export interface IWalletProvision { containerName: string; agentType: AgentType; orgName: string; - genesisUrl: string; + indyLedger: string; protocol: string; afjVersion: string; tenant: boolean; diff --git a/apps/agent-service/src/agent-service.controller.ts b/apps/agent-service/src/agent-service.controller.ts index e85fcd6ba..1d4a99192 100644 --- a/apps/agent-service/src/agent-service.controller.ts +++ b/apps/agent-service/src/agent-service.controller.ts @@ -106,4 +106,22 @@ export class AgentServiceController { async getProofFormData(payload: { url: string, apiKey: string }): Promise { return this.agentServiceService.getProofFormData(payload.url, payload.apiKey); } + + @MessagePattern({ cmd: 'agent-schema-endorsement-request' }) + async schemaEndorsementRequest(payload: { url: string, apiKey: string, requestSchemaPayload:object }): Promise { + return this.agentServiceService.schemaEndorsementRequest(payload.url, payload.apiKey, payload.requestSchemaPayload); + } + @MessagePattern({ cmd: 'agent-credDef-endorsement-request' }) + async credDefEndorsementRequest(payload: { url: string, apiKey: string, requestSchemaPayload:object }): Promise { + return this.agentServiceService.credDefEndorsementRequest(payload.url, payload.apiKey, payload.requestSchemaPayload); + } + + @MessagePattern({ cmd: 'agent-sign-transaction' }) + async signTransaction(payload: { url: string, apiKey: string, signEndorsementPayload:object }): Promise { + return this.agentServiceService.signTransaction(payload.url, payload.apiKey, payload.signEndorsementPayload); + } + @MessagePattern({ cmd: 'agent-submit-transaction' }) + async submitTransaction(payload: { url: string, apiKey: string, submitEndorsementPayload:object }): Promise { + return this.agentServiceService.sumbitTransaction(payload.url, payload.apiKey, payload.submitEndorsementPayload); + } } diff --git a/apps/agent-service/src/agent-service.service.ts b/apps/agent-service/src/agent-service.service.ts index b1a818b26..557960456 100644 --- a/apps/agent-service/src/agent-service.service.ts +++ b/apps/agent-service/src/agent-service.service.ts @@ -122,7 +122,7 @@ export class AgentServiceService { return internalIp; } catch (error) { this.logger.error(`error in valid internal ip : ${JSON.stringify(error)}`); - throw new RpcException(error.response); + throw new RpcException(error.response ? error.response : error); } } @@ -133,9 +133,11 @@ export class AgentServiceService { agentSpinupDto.agentType = agentSpinupDto.agentType ? agentSpinupDto.agentType : 1; agentSpinupDto.tenant = agentSpinupDto.tenant ? agentSpinupDto.tenant : false; + agentSpinupDto.ledgerId = !agentSpinupDto.ledgerId || 0 === agentSpinupDto.ledgerId.length ? [1] : agentSpinupDto.ledgerId; + const platformConfig: platform_config = await this.agentServiceRepository.getPlatformConfigDetails(); - const ledgerDetails: ledgers = await this.agentServiceRepository.getGenesisUrl(agentSpinupDto.ledgerId); + const ledgerDetails: ledgers[] = await this.agentServiceRepository.getGenesisUrl(agentSpinupDto.ledgerId); const orgData: organisation = await this.agentServiceRepository.getOrgDetails(agentSpinupDto.orgId); if (!orgData) { @@ -177,6 +179,18 @@ export class AgentServiceService { controllerIp ); + const ledgerArray = []; + for (const iterator of ledgerDetails) { + const ledgerJson = {}; + + ledgerJson["genesisTransactions"] = iterator.poolConfig; + ledgerJson["indyNamespace"] = iterator.indyNamespace; + + ledgerArray.push(ledgerJson); + } + + const ledgerString = JSON.stringify(ledgerArray); + const escapedJsonString = ledgerString.replace(/"/g, '\\"'); if (agentSpinupDto.agentType === AgentType.ACAPY) { // TODO: ACA-PY Agent Spin-Up @@ -197,7 +211,7 @@ export class AgentServiceService { containerName, agentType: AgentType.AFJ, orgName: orgData.name, - genesisUrl: ledgerDetails?.poolConfig, + indyLedger: escapedJsonString, afjVersion: process.env.AFJ_VERSION, protocol: process.env.API_GATEWAY_PROTOCOL, tenant: agentSpinupDto.tenant @@ -215,7 +229,7 @@ export class AgentServiceService { socket.emit('agent-spinup-process-initiated', { clientId: agentSpinupDto.clientSocketId }); } - await this._agentSpinup(walletProvisionPayload, agentSpinupDto, orgApiKey, orgData, user, socket); + await this._agentSpinup(walletProvisionPayload, agentSpinupDto, orgApiKey, orgData, user, socket, agentSpinupDto.ledgerId); const agentStatusResponse = { agentSpinupStatus: 1 }; @@ -237,7 +251,7 @@ export class AgentServiceService { } } - async _agentSpinup(walletProvisionPayload: IWalletProvision, agentSpinupDto: IAgentSpinupDto, orgApiKey: string, orgData: organisation, user: IUserRequestInterface, socket): Promise { + async _agentSpinup(walletProvisionPayload: IWalletProvision, agentSpinupDto: IAgentSpinupDto, orgApiKey: string, orgData: organisation, user: IUserRequestInterface, socket, ledgerId: number[]): Promise { try { const agentSpinUpResponse = new Promise(async (resolve, _reject) => { @@ -274,7 +288,8 @@ export class AgentServiceService { agentsTypeId: AgentType.AFJ, orgId: orgData.id, walletName: agentSpinupDto.walletName, - clientSocketId: agentSpinupDto.clientSocketId + clientSocketId: agentSpinupDto.clientSocketId, + ledgerId }; if (agentEndPoint && agentSpinupDto.clientSocketId) { @@ -344,7 +359,8 @@ export class AgentServiceService { orgId: payload.orgId, agentEndPoint: payload.agentEndPoint, agentId: payload.agentId, - orgAgentTypeId: OrgAgentType.DEDICATED + orgAgentTypeId: OrgAgentType.DEDICATED, + ledgerId: payload.ledgerId }; @@ -470,14 +486,10 @@ export class AgentServiceService { async _createTenant(payload: ITenantDto, user: IUserRequestInterface): Promise { try { + payload.ledgerId = !payload.ledgerId || 0 === payload.ledgerId.length ? [1] : payload.ledgerId; + + const ledgerDetails: ledgers[] = await this.agentServiceRepository.getGenesisUrl(payload.ledgerId); const sharedAgentSpinUpResponse = new Promise(async (resolve, _reject) => { - const { label, seed } = payload; - const createTenantOptions = { - config: { - label - }, - seed - }; const socket = await io(`${process.env.SOCKET_HOST}`, { reconnection: true, @@ -504,45 +516,58 @@ export class AgentServiceService { throw new NotFoundException('Platform-admin agent is not spun-up'); } - const apiKey = ''; + let tenantDetails; 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; - }); + for (const iterator of ledgerDetails) { + const { label, seed } = payload; + const createTenantOptions = { + config: { + label + }, + seed, + method: iterator.indyNamespace + }; + const apiKey = ''; + 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, + ledgerId: payload.ledgerId + }; + + if (payload.clientSocketId) { + socket.emit('agent-spinup-process-completed', { clientId: payload.clientSocketId }); + } - 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); - if (payload.clientSocketId) { - socket.emit('agent-spinup-process-completed', { clientId: payload.clientSocketId }); - } + if (payload.clientSocketId) { + socket.emit('invitation-url-creation-started', { clientId: payload.clientSocketId }); + } - const saveTenant = await this.agentServiceRepository.storeOrgAgentDetails(storeOrgAgentData); + await this._createLegacyConnectionInvitation(payload.orgId, user, storeOrgAgentData.walletName); - if (payload.clientSocketId) { - socket.emit('invitation-url-creation-started', { clientId: payload.clientSocketId }); - } - - await this._createLegacyConnectionInvitation(payload.orgId, user, storeOrgAgentData.walletName); + if (payload.clientSocketId) { + socket.emit('invitation-url-creation-success', { clientId: payload.clientSocketId }); + } - if (payload.clientSocketId) { - socket.emit('invitation-url-creation-success', { clientId: payload.clientSocketId }); + resolve(saveTenant); } - resolve(saveTenant); } else { throw new InternalServerErrorException('Agent not able to spin-up'); } @@ -573,7 +598,7 @@ export class AgentServiceService { }); socket.emit('error-in-wallet-creation-process', { clientId: payload.clientSocketId, error }); } - throw new RpcException(error.response); + throw new RpcException(error.response ? error.response : error); } } @@ -599,16 +624,12 @@ export class AgentServiceService { } else if (2 === payload.agentType) { - const url = `${payload.agentEndPoint}${CommonConstants.URL_SHAGENT_WITH_TENANT_AGENT}`; + const url = `${payload.agentEndPoint}${CommonConstants.URL_SHAGENT_CREATE_SCHEMA}`.replace('#', `${payload.tenantId}`); const schemaPayload = { - tenantId: payload.tenantId, - method: 'registerSchema', - payload: { - attributes: payload.payload.attributes, - version: payload.payload.version, - name: payload.payload.name, - issuerId: payload.payload.issuerId - } + 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) => { @@ -636,15 +657,9 @@ export class AgentServiceService { }); } 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 } }) + const url = `${payload.agentEndPoint}${CommonConstants.URL_SHAGENT_GET_SCHEMA}`.replace('@', `${payload.payload.schemaId}`).replace('#', `${payload.tenantId}`); + + schemaResponse = await this.commonService.httpGet(url, { headers: { 'x-api-key': payload.apiKey } }) .then(async (schema) => { this.logger.debug(`API Response Data: ${JSON.stringify(schema)}`); return schema; @@ -674,15 +689,11 @@ export class AgentServiceService { }); } else if (2 === payload.agentType) { - const url = `${payload.agentEndPoint}${CommonConstants.URL_SHAGENT_WITH_TENANT_AGENT}`; + const url = `${payload.agentEndPoint}${CommonConstants.URL_SHAGENT_CREATE_CRED_DEF}`.replace('#', `${payload.tenantId}`); const credDefPayload = { - tenantId: payload.tenantId, - method: 'registerCredentialDefinition', - payload: { - tag: payload.payload.tag, - schemaId: payload.payload.schemaId, - issuerId: payload.payload.issuerId - } + 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) => { @@ -710,15 +721,8 @@ export class AgentServiceService { }); } 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 } }) + const url = `${payload.agentEndPoint}${CommonConstants.URL_SHAGENT_GET_CRED_DEF}`.replace('@', `${payload.payload.credentialDefinitionId}`).replace('#', `${payload.tenantId}`); + credDefResponse = await this.commonService.httpGet(url, { headers: { 'x-api-key': payload.apiKey } }) .then(async (credDef) => { this.logger.debug(`API Response Data: ${JSON.stringify(credDef)}`); return credDef; @@ -739,7 +743,7 @@ export class AgentServiceService { return data; } catch (error) { this.logger.error(`Error in connection Invitation in agent service : ${JSON.stringify(error)}`); - throw new RpcException(error); + throw new RpcException(error.response ? error.response : error); } } @@ -761,7 +765,7 @@ export class AgentServiceService { return getProofPresentationsData; } catch (error) { this.logger.error(`Error in proof presentations in agent service : ${JSON.stringify(error)}`); - throw new RpcException(error); + throw new RpcException(error.response ? error.response : error); } } @@ -783,7 +787,7 @@ export class AgentServiceService { return getProofPresentationById; } catch (error) { this.logger.error(`Error in proof presentation by id in agent service : ${JSON.stringify(error)}`); - throw new RpcException(error); + throw new RpcException(error.response ? error.response : error); } } @@ -805,7 +809,7 @@ export class AgentServiceService { return sendProofRequest; } catch (error) { this.logger.error(`Error in send proof request in agent service : ${JSON.stringify(error)}`); - throw new RpcException(error); + throw new RpcException(error.response ? error.response : error); } } @@ -817,7 +821,7 @@ export class AgentServiceService { return verifyPresentation; } catch (error) { this.logger.error(`Error in verify proof presentation in agent service : ${JSON.stringify(error)}`); - throw new RpcException(error); + throw new RpcException(error.response ? error.response : error); } } @@ -860,7 +864,7 @@ export class AgentServiceService { } catch (error) { this.logger.error(`Agent health details : ${JSON.stringify(error)}`); - throw new RpcException(error); + throw new RpcException(error.response ? error.response : error); } } @@ -872,7 +876,7 @@ export class AgentServiceService { return sendProofRequest; } catch (error) { this.logger.error(`Error in send out of band proof request in agent service : ${JSON.stringify(error)}`); - throw new RpcException(error); + throw new RpcException(error.response ? error.response : error); } } @@ -884,8 +888,60 @@ export class AgentServiceService { return getProofFormData; } catch (error) { this.logger.error(`Error in get proof form data in agent service : ${JSON.stringify(error)}`); - throw new RpcException(error); + throw new RpcException(error.response ? error.response : error); + } + } + + async schemaEndorsementRequest(url: string, apiKey: string, requestSchemaPayload: object): Promise { + try { + const schemaRequest = await this.commonService + .httpPost(url, requestSchemaPayload, { headers: { 'x-api-key': apiKey } }) + .then(async response => response); + return schemaRequest; + } catch (error) { + this.logger.error(`Error in schema endorsement request in agent service : ${JSON.stringify(error)}`); + throw new RpcException(error.response ? error.response : error); } } + + async credDefEndorsementRequest(url: string, apiKey: string, requestSchemaPayload: object): Promise { + try { + const credDefRequest = await this.commonService + .httpPost(url, requestSchemaPayload, { headers: { 'x-api-key': apiKey } }) + .then(async response => response); + return credDefRequest; + } catch (error) { + this.logger.error(`Error in credential-definition endorsement request in agent service : ${JSON.stringify(error)}`); + throw new RpcException(error.response ? error.response : error); + } + } + + async signTransaction(url: string, apiKey: string, signEndorsementPayload: object): Promise { + try { + const signEndorsementTransaction = await this.commonService + .httpPost(url, signEndorsementPayload, { headers: { 'x-api-key': apiKey } }) + .then(async response => response); + + return signEndorsementTransaction; + } catch (error) { + this.logger.error(`Error in sign transaction in agent service : ${JSON.stringify(error)}`); + throw new RpcException(error.response ? error.response : error); + } + } + + async sumbitTransaction(url: string, apiKey: string, submitEndorsementPayload: object): Promise { + try { + + const signEndorsementTransaction = await this.commonService + .httpPost(url, submitEndorsementPayload, { headers: { 'x-api-key': apiKey } }) + .then(async response => response); + + return signEndorsementTransaction; + } catch (error) { + this.logger.error(`Error in sumbit transaction in agent service : ${JSON.stringify(error)}`); + throw new RpcException(error.response ? error.response : error); + } + } + } diff --git a/apps/agent-service/src/interface/agent-service.interface.ts b/apps/agent-service/src/interface/agent-service.interface.ts index 81ed67264..3386889bb 100644 --- a/apps/agent-service/src/interface/agent-service.interface.ts +++ b/apps/agent-service/src/interface/agent-service.interface.ts @@ -7,8 +7,8 @@ export interface IAgentSpinupDto { walletPassword: string; seed: string; orgId: number; + ledgerId?: number[]; agentType?: AgentType; - ledgerId?: number; transactionApproval?: boolean; clientSocketId?: string tenant?: boolean; @@ -17,8 +17,10 @@ export interface IAgentSpinupDto { export interface ITenantDto { label: string; seed: string; - tenantId?: string; + ledgerId?: number[]; + method: string; orgId: number; + tenantId?: string; clientSocketId?: string; } @@ -103,7 +105,7 @@ export interface IWalletProvision { containerName: string; agentType: AgentType; orgName: string; - genesisUrl: string; + indyLedger: string; afjVersion: string; protocol: string; tenant: boolean; @@ -135,6 +137,7 @@ export interface IStoreOrgAgentDetails { agentId?: number; orgAgentTypeId?: OrgAgentType; tenantId?: string; + ledgerId?: number[]; } diff --git a/apps/agent-service/src/main.ts b/apps/agent-service/src/main.ts index d936711a9..53d5fbba0 100644 --- a/apps/agent-service/src/main.ts +++ b/apps/agent-service/src/main.ts @@ -27,7 +27,8 @@ async function bootstrap(): Promise { walletPassword: process.env.PLATFORM_WALLET_PASSWORD, seed: process.env.PLATFORM_SEED, orgId: parseInt(process.env.PLATFORM_ID), - tenant: true + tenant: true, + ledgerId: [1, 2] }; const agentService = app.get(AgentServiceService); diff --git a/apps/agent-service/src/repositories/agent-service.repository.ts b/apps/agent-service/src/repositories/agent-service.repository.ts index eda85e851..90d050471 100644 --- a/apps/agent-service/src/repositories/agent-service.repository.ts +++ b/apps/agent-service/src/repositories/agent-service.repository.ts @@ -29,12 +29,14 @@ export class AgentServiceRepository { * @param id * @returns */ - async getGenesisUrl(id: number): Promise { + async getGenesisUrl(ledgerId: number[]): Promise { try { - const genesisData = await this.prisma.ledgers.findFirst({ + const genesisData = await this.prisma.ledgers.findMany({ where: { - id + id: { + in: ledgerId + } } }); return genesisData; @@ -86,7 +88,7 @@ export class AgentServiceRepository { agentId: storeOrgAgentDetails.agentId ? storeOrgAgentDetails.agentId : null, orgAgentTypeId: storeOrgAgentDetails.orgAgentTypeId ? storeOrgAgentDetails.orgAgentTypeId : null, tenantId: storeOrgAgentDetails.tenantId ? storeOrgAgentDetails.tenantId : null, - ledgerId: 1 + ledgerId: storeOrgAgentDetails.ledgerId[0] } }); } catch (error) { @@ -135,13 +137,13 @@ export class AgentServiceRepository { } } - /** - * Get agent details - * @param orgId - * @returns Agent health details - */ + /** + * Get agent details + * @param orgId + * @returns Agent health details + */ // eslint-disable-next-line camelcase - async getOrgAgentDetails(orgId: number): Promise { + async getOrgAgentDetails(orgId: number): Promise { try { const oranizationAgentDetails = await this.prisma.org_agents.findFirst({ where: { diff --git a/apps/api-gateway/common/exception-handler.ts b/apps/api-gateway/common/exception-handler.ts index 45a87a1cb..27a78f391 100644 --- a/apps/api-gateway/common/exception-handler.ts +++ b/apps/api-gateway/common/exception-handler.ts @@ -1,8 +1,9 @@ -import { Catch, ArgumentsHost, HttpException, HttpStatus } from '@nestjs/common'; +import { Catch, ArgumentsHost, HttpException, HttpStatus, Logger } from '@nestjs/common'; import { BaseExceptionFilter } from '@nestjs/core'; @Catch() export class CustomExceptionFilter extends BaseExceptionFilter { + private readonly logger = new Logger(); catch(exception: HttpException, host: ArgumentsHost): void { const ctx = host.switchToHttp(); const response = ctx.getResponse(); @@ -12,16 +13,61 @@ export class CustomExceptionFilter extends BaseExceptionFilter { status = exception.getStatus(); } + this.logger.error(`exception ::: ${JSON.stringify(exception)}`); + if ("Cannot read properties of undefined (reading 'response')" === exception.message) { exception.message = 'Oops! Something went wrong. Please try again'; } - const errorResponse = { - statusCode: status, - message: exception.message || 'Internal server error', - error: exception.message - }; + let errorResponse; + if (exception && exception["statusCode"] === HttpStatus.INTERNAL_SERVER_ERROR) { + errorResponse = { + statusCode: status, + message: 'Oops! Something went wrong. Please try again', + error: 'Oops! Something went wrong. Please try again' + }; + } else if (exception && exception["error"] && exception["error"].message && (exception["error"].statusCode || exception["error"].code)) { + + const statusCode = exception["error"].statusCode || exception["error"].code || status; + errorResponse = { + statusCode, + message: exception["error"].message || 'Internal server error', + error: exception["error"].message || 'Internal server error' + }; + } else if (exception && exception["statusCode"] === undefined && status === HttpStatus.INTERNAL_SERVER_ERROR) { + errorResponse = { + statusCode: status, + message: 'Oops! Something went wrong. Please try again', + error: 'Oops! Something went wrong. Please try again' + }; + } else { + if (exception && exception["response"] && exception.message) { + + if (Array.isArray(exception["response"].message)) { + + errorResponse = { + statusCode: exception["statusCode"] ? exception["statusCode"] : status, + message: exception.message ? exception.message : 'Internal server error', + error: exception["response"].message ? exception["response"].message : exception["response"] ? exception["response"] : 'Internal server error' + }; + } else { + errorResponse = { + statusCode: exception["statusCode"] ? exception["statusCode"] : status, + message: exception["response"].message ? exception["response"].message : exception["response"] ? exception["response"] : 'Internal server error', + error: exception["response"].message ? exception["response"].message : exception["response"] ? exception["response"] : 'Internal server error' + }; + } + } else if (exception && exception.message) { + + errorResponse = { + statusCode: exception["statusCode"] ? exception["statusCode"] : status, + message: exception.message || 'Internal server error', + error: exception.message || 'Internal server error' + }; + + } + } - response.status(status).json(errorResponse); + response.status(errorResponse.statusCode).json(errorResponse); } } \ No newline at end of file diff --git a/apps/api-gateway/src/agent-service/agent-service.controller.ts b/apps/api-gateway/src/agent-service/agent-service.controller.ts index 55a083404..d4b2890b6 100644 --- a/apps/api-gateway/src/agent-service/agent-service.controller.ts +++ b/apps/api-gateway/src/agent-service/agent-service.controller.ts @@ -11,10 +11,10 @@ import { HttpStatus, Res, Get, - Query, - UseFilters + UseFilters, + Param } from '@nestjs/common'; -import { ApiTags, ApiResponse, ApiOperation, ApiUnauthorizedResponse, ApiForbiddenResponse, ApiBearerAuth, ApiQuery } from '@nestjs/swagger'; +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'; @@ -30,43 +30,74 @@ import { user } from '@prisma/client'; import { CreateTenantDto } from './dto/create-tenant.dto'; import { User } from '../authz/decorators/user.decorator'; import { CustomExceptionFilter } from 'apps/api-gateway/common/exception-handler'; +import { Roles } from '../authz/decorators/roles.decorator'; +import { OrgRoles } from 'libs/org-roles/enums'; +import { OrgRolesGuard } from '../authz/guards/org-roles.guard'; +const seedLength = 32; @UseFilters(CustomExceptionFilter) -@Controller('agent-service') +@Controller() @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(); + @Get('/orgs/:orgId/agents/health') + @ApiOperation({ + summary: 'Get the agent health details', + description: 'Get the agent health details' + }) + @UseGuards(AuthGuard('jwt')) + async getAgentHealth(@User() reqUser: user, @Param('orgId') orgId: number, @Res() res: Response): Promise { + const agentData = await this.agentService.getAgentHealth(reqUser, orgId); + + const finalResponse: IResponseType = { + statusCode: HttpStatus.OK, + message: ResponseMessages.agent.success.health, + data: agentData.response + }; + + return res.status(HttpStatus.OK).json(finalResponse); + + } + /** * * @param agentSpinupDto * @param user * @returns */ - @Post('/spinup') + @Post('/orgs/:orgId/agents/spinup') @ApiOperation({ summary: 'Agent spinup', description: 'Create a new agent spin up.' }) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + @Roles(OrgRoles.OWNER, OrgRoles.ADMIN) @ApiResponse({ status: 201, description: 'Success', type: ApiResponseDto }) async agentSpinup( @Body() agentSpinupDto: AgentSpinupDto, + @Param('orgId') orgId: number, @GetUser() user: user, @Res() res: Response ): Promise>> { + if (seedLength !== agentSpinupDto.seed.length) { + throw new BadRequestException(`seed must be at most 32 characters.`); + } + 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)}`); + + agentSpinupDto.orgId = orgId; const agentDetails = await this.agentService.agentSpinup(agentSpinupDto, user); const finalResponse: IResponseType = { @@ -78,17 +109,27 @@ export class AgentController { return res.status(HttpStatus.CREATED).json(finalResponse); } - @Post('/tenant') + @Post('/orgs/:orgId/agents/wallet') @ApiOperation({ summary: 'Shared Agent', description: 'Create a shared agent.' }) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + @Roles(OrgRoles.OWNER, OrgRoles.ADMIN) @ApiResponse({ status: 201, description: 'Success', type: ApiResponseDto }) async createTenant( + @Param('orgId') orgId: number, @Body() createTenantDto: CreateTenantDto, @GetUser() user: user, @Res() res: Response ): Promise { + + createTenantDto.orgId = orgId; + + if (seedLength !== createTenantDto.seed.length) { + throw new BadRequestException(`seed must be at most 32 characters.`); + } + const tenantDetails = await this.agentService.createTenant(createTenantDto, user); const finalResponse: IResponseType = { @@ -99,28 +140,4 @@ export class AgentController { return res.status(HttpStatus.CREATED).json(finalResponse); } - - @Get('/health') - @ApiOperation({ - summary: 'Fetch agent details', - description: 'Fetch agent health details' - }) - @ApiQuery({ - name: 'orgId', - type: Number, - required: false - }) - async getAgentHealth(@User() reqUser: user, @Query('orgId') orgId: number, @Res() res: Response): Promise { - const agentData = await this.agentService.getAgentHealth(reqUser, orgId); - - const finalResponse: IResponseType = { - statusCode: HttpStatus.OK, - message: ResponseMessages.agent.success.health, - data: agentData.response - }; - - return res.status(HttpStatus.OK).json(finalResponse); - - } - -} +} \ No newline at end of file diff --git a/apps/api-gateway/src/agent-service/dto/agent-service.dto.ts b/apps/api-gateway/src/agent-service/dto/agent-service.dto.ts index c05383e9e..a4227b193 100644 --- a/apps/api-gateway/src/agent-service/dto/agent-service.dto.ts +++ b/apps/api-gateway/src/agent-service/dto/agent-service.dto.ts @@ -1,13 +1,15 @@ import { trim } from '@credebl/common/cast.helper'; -import { ApiProperty } from '@nestjs/swagger'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { Transform } from 'class-transformer'; -import { IsBoolean, IsNotEmpty, IsNumber, IsOptional, IsString, Matches, MaxLength, MinLength } from 'class-validator'; +import { IsBoolean, IsNotEmpty, IsOptional, IsString, Matches, MaxLength, MinLength, IsArray } from 'class-validator'; const regex = /^[a-zA-Z0-9 ]*$/; export class AgentSpinupDto { + orgId: number; + @ApiProperty() @Transform(({ value }) => trim(value)) - @IsNotEmpty({ message: 'walletName is required'}) + @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.' }) @@ -22,10 +24,10 @@ export class AgentSpinupDto { @IsNotEmpty({ message: 'Password is required.' }) walletPassword: string; - + @ApiProperty() @Transform(({ value }) => trim(value)) - @IsNotEmpty({ message: 'seed is required'}) + @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*$/, { @@ -33,28 +35,28 @@ export class AgentSpinupDto { }) seed: string; - @ApiProperty() - @IsNumber() - orgId: number; - - @ApiProperty() + @ApiProperty({ example: [1] }) @IsOptional() - @IsNumber() - ledgerId?: number; + @IsArray({ message: 'ledgerId must be an array' }) + @IsNotEmpty({ message: 'please provide valid ledgerId' }) + ledgerId?: number[]; @ApiProperty() - @IsOptional() + @IsOptional() + @ApiPropertyOptional() clientSocketId?: string; @ApiProperty() @IsOptional() - @IsBoolean() + @IsBoolean() + @ApiPropertyOptional() tenant?: boolean; - + @ApiProperty() @IsOptional() + @ApiPropertyOptional() @Transform(({ value }) => trim(value)) - @IsNotEmpty({ message: 'agentType is required'}) + @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.' }) @@ -62,10 +64,11 @@ export class AgentSpinupDto { @ApiProperty() @IsOptional() + @ApiPropertyOptional() @Transform(({ value }) => trim(value)) - @IsNotEmpty({ message: 'transactionApproval is required'}) + @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; -} +} \ No newline at end of file diff --git a/apps/api-gateway/src/agent-service/dto/create-tenant.dto.ts b/apps/api-gateway/src/agent-service/dto/create-tenant.dto.ts index 6ed0aa6ad..f0757532a 100644 --- a/apps/api-gateway/src/agent-service/dto/create-tenant.dto.ts +++ b/apps/api-gateway/src/agent-service/dto/create-tenant.dto.ts @@ -1,13 +1,13 @@ import { trim } from '@credebl/common/cast.helper'; -import { ApiProperty } from '@nestjs/swagger'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { Transform } from 'class-transformer'; -import { IsNotEmpty, IsNumber, IsString, Matches, MaxLength, MinLength, IsOptional } from 'class-validator'; +import { MaxLength, IsString, MinLength, Matches, IsNotEmpty, IsOptional, IsArray } 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.' }) + @IsString({ message: 'label must be in string format.' }) + @Transform(({ value }) => value.trim()) @MinLength(2, { message: 'Minimum length for label must be 2 characters.' }) @Matches(labelRegex, { message: 'Label must not contain special characters.' }) @Matches(/^\S*$/, { @@ -16,20 +16,25 @@ export class CreateTenantDto { label: string; @ApiProperty() + @MaxLength(32, { message: 'seed must be at most 32 characters.' }) @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() + @ApiProperty({ example: [1] }) + @IsOptional() + @IsArray({ message: 'ledgerId must be an array' }) + @IsNotEmpty({ message: 'please provide valid ledgerId' }) + ledgerId?: number[]; + orgId: number; @ApiProperty() @IsOptional() + @ApiPropertyOptional() clientSocketId?: string; } \ No newline at end of file diff --git a/apps/api-gateway/src/app.module.ts b/apps/api-gateway/src/app.module.ts index d710b05fb..792999579 100644 --- a/apps/api-gateway/src/app.module.ts +++ b/apps/api-gateway/src/app.module.ts @@ -11,7 +11,6 @@ import { CredentialDefinitionModule } from './credential-definition/credential-d 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'; @@ -20,6 +19,7 @@ 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'; +import { EcosystemModule } from './ecosystem/ecosystem.module'; @Module({ imports: [ @@ -41,7 +41,8 @@ import { ConnectionModule } from './connection/connection.module'; OrganizationModule, UserModule, ConnectionModule, - IssuanceModule + IssuanceModule, + EcosystemModule ], controllers: [AppController], providers: [AppService] @@ -80,7 +81,6 @@ export class AppModule { ) .forRoutes( AgentController, - PlatformController, RevocationController ); } diff --git a/apps/api-gateway/src/authz/authz.controller.ts b/apps/api-gateway/src/authz/authz.controller.ts index 63b26cfa5..c2100717b 100644 --- a/apps/api-gateway/src/authz/authz.controller.ts +++ b/apps/api-gateway/src/authz/authz.controller.ts @@ -1,17 +1,150 @@ import { + BadRequestException, + Body, Controller, - Logger + Get, + HttpStatus, + Logger, + Post, + Query, + Res, + UnauthorizedException, + UseFilters } from '@nestjs/common'; import { AuthzService } from './authz.service'; -// import { CommonService } from "@credebl/common"; import { CommonService } from '../../../../libs/common/src/common.service'; +import { ApiBody, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { ApiResponseDto } from '../dtos/apiResponse.dto'; +import { UserEmailVerificationDto } from '../user/dto/create-user.dto'; +import IResponseType from '@credebl/common/interfaces/response.interface'; +import { ResponseMessages } from '@credebl/common/response-messages'; +import { Response } from 'express'; +import { EmailVerificationDto } from '../user/dto/email-verify.dto'; +import { AuthTokenResponse } from './dtos/auth-token-res.dto'; +import { LoginUserDto } from '../user/dto/login-user.dto'; +import { AddUserDetails } from '../user/dto/add-user.dto'; +import { CustomExceptionFilter } from 'apps/api-gateway/common/exception-handler'; -@Controller('authz') +@Controller('auth') +@ApiTags('auth') +@UseFilters(CustomExceptionFilter) export class AuthzController { private logger = new Logger('AuthzController'); constructor(private readonly authzService: AuthzService, private readonly commonService: CommonService) { } -} + /** + * + * @param query + * @param res + * @returns User email verified + */ + @Get('/verify') + @ApiResponse({ status: 200, description: 'Success', type: ApiResponseDto }) + @ApiOperation({ summary: 'Verify user’s email', description: 'Verify user’s email' }) + async verifyEmail(@Query() query: EmailVerificationDto, @Res() res: Response): Promise { + await this.authzService.verifyEmail(query); + const finalResponse: IResponseType = { + statusCode: HttpStatus.OK, + message: ResponseMessages.user.success.emaiVerified + }; + + return res.status(HttpStatus.OK).json(finalResponse); + + } + + /** + * + * @param email + * @param res + * @returns Email sent success + */ + @Post('/verification-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.authzService.sendVerificationMail(userEmailVerificationDto); + const finalResponse: IResponseType = { + statusCode: HttpStatus.CREATED, + message: ResponseMessages.user.success.sendVerificationCode + }; + return res.status(HttpStatus.CREATED).json(finalResponse); + } + + /** + * + * @param email + * @param userInfo + * @param res + * @returns Add new user + */ + @Post('/signup') + @ApiOperation({ summary: 'Register new user to platform', description: 'Register new user to platform' }) + async addUserDetails(@Body() userInfo: AddUserDetails, @Res() res: Response): Promise { + let finalResponse; + let userDetails; + + if (false === userInfo.isPasskey) { + + const decryptedPassword = this.commonService.decryptPassword(userInfo.password); + if (8 <= decryptedPassword.length && 50 >= decryptedPassword.length) { + this.commonService.passwordValidation(decryptedPassword); + userInfo.password = decryptedPassword; + userDetails = await this.authzService.addUserDetails(userInfo); + finalResponse = { + statusCode: HttpStatus.CREATED, + message: ResponseMessages.user.success.create, + data: userDetails.response + }; + } else { + throw new BadRequestException('Password name must be between 8 to 50 Characters'); + } + } else { + + userDetails = await this.authzService.addUserDetails(userInfo); + finalResponse = { + statusCode: HttpStatus.CREATED, + message: ResponseMessages.user.success.create, + data: userDetails.response + }; + } + return res.status(HttpStatus.CREATED).json(finalResponse); + + } + + /** + * + * @param loginUserDto + * @param res + * @returns User access token details + */ + @Post('/signin') + @ApiOperation({ + summary: 'Authenticate the user for the access', + description: 'Authenticate the user for the access' + }) + @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.authzService.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`); + } + } + +} \ No newline at end of file diff --git a/apps/api-gateway/src/authz/authz.module.ts b/apps/api-gateway/src/authz/authz.module.ts index 1e6b80cd2..3c9448584 100644 --- a/apps/api-gateway/src/authz/authz.module.ts +++ b/apps/api-gateway/src/authz/authz.module.ts @@ -16,6 +16,7 @@ import { SupabaseService } from '@credebl/supabase'; import { UserModule } from '../user/user.module'; import { UserService } from '../user/user.service'; import { VerificationService } from '../verification/verification.service'; +import { EcosystemService } from '../ecosystem/ecosystem.service'; //import { WebhookService } from "../../../platform-service/src/webhook/webhook.service"; @@ -48,7 +49,8 @@ import { VerificationService } from '../verification/verification.service'; AgentService, CommonService, UserService, - SupabaseService + SupabaseService, + EcosystemService ], exports: [ PassportModule, diff --git a/apps/api-gateway/src/authz/authz.service.ts b/apps/api-gateway/src/authz/authz.service.ts index ab5cd587c..317712669 100644 --- a/apps/api-gateway/src/authz/authz.service.ts +++ b/apps/api-gateway/src/authz/authz.service.ts @@ -6,6 +6,9 @@ import { WebSocketServer } from '@nestjs/websockets'; +import { UserEmailVerificationDto } from '../user/dto/create-user.dto'; +import { EmailVerificationDto } from '../user/dto/email-verify.dto'; +import { AddUserDetails } from '../user/dto/add-user.dto'; @Injectable() @@ -25,4 +28,24 @@ export class AuthzService extends BaseService { return this.sendNats(this.authServiceProxy, 'get-user-by-keycloakUserId', keycloakUserId); } -} + async sendVerificationMail(userEmailVerificationDto: UserEmailVerificationDto): Promise { + const payload = { userEmailVerificationDto }; + return this.sendNats(this.authServiceProxy, 'send-verification-mail', payload); + } + + async verifyEmail(param: EmailVerificationDto): Promise { + const payload = { param }; + return this.sendNats(this.authServiceProxy, 'user-email-verification', payload); + + } + + async login(email: string, password?: string, isPasskey = false): Promise<{ response: object }> { + const payload = { email, password, isPasskey }; + return this.sendNats(this.authServiceProxy, 'user-holder-login', payload); + } + + async addUserDetails(userInfo: AddUserDetails): Promise<{ response: string }> { + const payload = { userInfo }; + return this.sendNats(this.authServiceProxy, 'add-user', payload); + } +} \ No newline at end of file diff --git a/apps/api-gateway/src/authz/decorators/roles.decorator.ts b/apps/api-gateway/src/authz/decorators/roles.decorator.ts index a172bdaee..5fd3237ab 100644 --- a/apps/api-gateway/src/authz/decorators/roles.decorator.ts +++ b/apps/api-gateway/src/authz/decorators/roles.decorator.ts @@ -1,9 +1,12 @@ import { CustomDecorator } from '@nestjs/common'; import { OrgRoles } from 'libs/org-roles/enums'; import { SetMetadata } from '@nestjs/common'; +import { EcosystemRoles } from '@credebl/enum/enum'; export const ROLES_KEY = 'roles'; +export const ECOSYSTEM_ROLES_KEY = 'ecosystem_roles'; export const Roles = (...roles: OrgRoles[]): CustomDecorator => SetMetadata(ROLES_KEY, roles); +export const EcosystemsRoles = (...roles: EcosystemRoles[]): CustomDecorator => SetMetadata(ECOSYSTEM_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/guards/ecosystem-roles.guard.ts b/apps/api-gateway/src/authz/guards/ecosystem-roles.guard.ts new file mode 100644 index 000000000..325c9893b --- /dev/null +++ b/apps/api-gateway/src/authz/guards/ecosystem-roles.guard.ts @@ -0,0 +1,64 @@ +import { CanActivate, ExecutionContext, Logger } from '@nestjs/common'; + +import { HttpException } from '@nestjs/common'; +import { HttpStatus } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; +import { ECOSYSTEM_ROLES_KEY } from '../decorators/roles.decorator'; +import { Reflector } from '@nestjs/core'; +import { EcosystemService } from '../../ecosystem/ecosystem.service'; +import { EcosystemRoles } from '@credebl/enum/enum'; + +@Injectable() +export class EcosystemRolesGuard implements CanActivate { + constructor( + private reflector: Reflector, + private readonly ecosystemService: EcosystemService // Inject the service + ) { } + + + private logger = new Logger('Ecosystem Role Guard'); + async canActivate(context: ExecutionContext): Promise { + const requiredRoles = this.reflector.getAllAndOverride(ECOSYSTEM_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.params.orgId || req.query.orgId || req.body.orgId) + && (req.params.ecosystemId || req.query.ecosystemId || req.body.ecosystemId)) { + + const orgId = req.params.orgId || req.query.orgId || req.body.orgId; + const ecosystemId = req.params.ecosystemId || req.query.ecosystemId || req.body.ecosystemId; + + + const ecosystemOrgData = await this.ecosystemService.fetchEcosystemOrg(ecosystemId, orgId); + + if (!ecosystemOrgData) { + throw new HttpException('Organization does not match', HttpStatus.FORBIDDEN); + } + + const {response} = ecosystemOrgData; + + user.ecosystemOrgRole = response['ecosystemRole']['name']; + + if (!user.ecosystemOrgRole) { + throw new HttpException('Ecosystem role not match', HttpStatus.FORBIDDEN); + } + + } else { + throw new HttpException('organization & ecosystem is required', HttpStatus.BAD_REQUEST); + } + + return requiredRoles.some((role) => user.ecosystemOrgRole === role); + + } +} \ No newline at end of file diff --git a/apps/api-gateway/src/authz/guards/org-roles.guard.ts b/apps/api-gateway/src/authz/guards/org-roles.guard.ts index 0c6dd349e..46c60aec7 100644 --- a/apps/api-gateway/src/authz/guards/org-roles.guard.ts +++ b/apps/api-gateway/src/authz/guards/org-roles.guard.ts @@ -9,7 +9,7 @@ import { Reflector } from '@nestjs/core'; @Injectable() export class OrgRolesGuard implements CanActivate { - constructor(private reflector: Reflector) { } // eslint-disable-next-line array-callback-return + constructor(private reflector: Reflector) { } // eslint-disable-next-line array-callback-return private logger = new Logger('Org Role Guard'); @@ -29,8 +29,8 @@ export class OrgRolesGuard implements CanActivate { const { user } = req; - if (req.query.orgId || req.body.orgId) { - const orgId = req.query.orgId || req.body.orgId; + if (req.params.orgId || req.query.orgId || req.body.orgId) { + const orgId = req.params.orgId || req.query.orgId || req.body.orgId; const specificOrg = user.userOrgRoles.find((orgDetails) => { if (!orgDetails.orgId) { @@ -44,12 +44,17 @@ export class OrgRolesGuard implements CanActivate { } user.selectedOrg = specificOrg; - user.selectedOrg.orgRoles = user.userOrgRoles.map(roleItem => roleItem.orgRole.name); - + // eslint-disable-next-line array-callback-return + user.selectedOrg.orgRoles = user.userOrgRoles.map((orgRoleItem) => { + if (orgRoleItem.orgId && orgRoleItem.orgId.toString() === orgId.toString()) { + return orgRoleItem.orgRole.name; + } + }); + } else { throw new HttpException('organization is required', HttpStatus.BAD_REQUEST); } return requiredRoles.some((role) => user.selectedOrg?.orgRoles.includes(role)); } -} +} \ No newline at end of file diff --git a/apps/api-gateway/src/connection/connection.controller.ts b/apps/api-gateway/src/connection/connection.controller.ts index d9ad6bf9a..d87f58721 100644 --- a/apps/api-gateway/src/connection/connection.controller.ts +++ b/apps/api-gateway/src/connection/connection.controller.ts @@ -14,41 +14,53 @@ import { Response } from 'express'; import { Connections } from './enums/connections.enum'; import { IUserRequest } from '@credebl/user-request/user-request.interface'; import { CustomExceptionFilter } from 'apps/api-gateway/common/exception-handler'; +import { OrgRoles } from 'libs/org-roles/enums'; +import { Roles } from '../authz/decorators/roles.decorator'; +import { OrgRolesGuard } from '../authz/guards/org-roles.guard'; @UseFilters(CustomExceptionFilter) @Controller() @ApiTags('connections') +@ApiBearerAuth() @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() + ) { } + + /** + * Description: Get connection by connectionId + * @param user + * @param connectionId + * @param orgId + * + */ + @Get('orgs/:orgId/connections/:connectionId') + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + @Roles(OrgRoles.OWNER, OrgRoles.ADMIN, OrgRoles.ISSUER, OrgRoles.VERIFIER, OrgRoles.MEMBER) + @ApiOperation({ + summary: `Get connections by connection Id`, + description: `Get connections by connection Id` + }) @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); + async getConnectionsById( + @User() user: IUserRequest, + @Param('connectionId') connectionId: string, + @Param('orgId') orgId: number, + @Res() res: Response + ): Promise { + const connectionsDetails = await this.connectionService.getConnectionsById(user, connectionId, orgId); + const finalResponse: IResponseType = { statusCode: HttpStatus.OK, - message: ResponseMessages.connection.success.create, - data: connectionData.response + message: ResponseMessages.connection.success.fetch, + data: connectionsDetails.response }; return res.status(HttpStatus.OK).json(finalResponse); - } - /** * Description: Get all connections * @param user @@ -58,14 +70,14 @@ export class ConnectionController { * @param orgId * */ - @Get('/connections') - @UseGuards(AuthGuard('jwt')) - @ApiBearerAuth() + @Get('/orgs/:orgId/connections') + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + @Roles(OrgRoles.OWNER, OrgRoles.ADMIN, OrgRoles.ISSUER, OrgRoles.VERIFIER, OrgRoles.MEMBER) @ApiOperation({ - summary: `Fetch all connections details`, - description: `Fetch all connections details` + summary: `Fetch all connection details`, + description: `Fetch all connection details` }) - @ApiResponse({ status: 201, description: 'Success', type: AuthTokenResponse }) + @ApiResponse({ status: 200, description: 'Success', type: AuthTokenResponse }) @ApiQuery( { name: 'outOfBandId', required: false } ) @@ -84,10 +96,6 @@ export class ConnectionController { @ApiQuery( { name: 'theirLabel', required: false } ) - @ApiQuery( - { name: 'orgId', required: true } - ) - async getConnections( @User() user: IUserRequest, @Query('outOfBandId') outOfBandId: string, @@ -96,7 +104,7 @@ export class ConnectionController { @Query('myDid') myDid: string, @Query('theirDid') theirDid: string, @Query('theirLabel') theirLabel: string, - @Query('orgId') orgId: number, + @Param('orgId') orgId: number, @Res() res: Response ): Promise { @@ -112,6 +120,34 @@ export class ConnectionController { return res.status(HttpStatus.OK).json(finalResponse); } + /** + * Create out-of-band connection legacy invitation + * @param connectionDto + * @param res + * @returns Created out-of-band connection invitation url + */ + @Post('/orgs/:orgId/connections') + @ApiOperation({ summary: 'Create outbound out-of-band connection (Legacy Invitation)', description: 'Create outbound out-of-band connection (Legacy Invitation)' }) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + @Roles(OrgRoles.OWNER, OrgRoles.ADMIN) + @ApiResponse({ status: 201, description: 'Success', type: AuthTokenResponse }) + async createLegacyConnectionInvitation( + @Param('orgId') orgId: number, + @Body() connectionDto: CreateConnectionDto, + @User() reqUser: IUserRequestInterface, + @Res() res: Response + ): Promise { + + connectionDto.orgId = orgId; + const connectionData = await this.connectionService.createLegacyConnectionInvitation(connectionDto, reqUser); + const finalResponse: IResponseType = { + statusCode: HttpStatus.CREATED, + message: ResponseMessages.connection.success.create, + data: connectionData.response + }; + return res.status(HttpStatus.CREATED).json(finalResponse); + + } /** * Catch connection webhook responses. @@ -132,70 +168,13 @@ export class ConnectionController { @Param('id') id: number, @Res() res: Response ): Promise { + this.logger.debug(`connectionDto ::: ${JSON.stringify(connectionDto)}`); const connectionData = await this.connectionService.getConnectionWebhook(connectionDto, id); const finalResponse: IResponseType = { - statusCode: HttpStatus.OK, + statusCode: HttpStatus.CREATED, 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.OK, - message: ResponseMessages.connection.success.fetch, - data: connectionsDetails.response - }; - return res.status(HttpStatus.OK).json(finalResponse); + return res.status(HttpStatus.CREATED).json(finalResponse); } } diff --git a/apps/api-gateway/src/connection/dtos/connection.dto.ts b/apps/api-gateway/src/connection/dtos/connection.dto.ts index e8bb7c55a..14d5d8f6e 100644 --- a/apps/api-gateway/src/connection/dtos/connection.dto.ts +++ b/apps/api-gateway/src/connection/dtos/connection.dto.ts @@ -1,4 +1,4 @@ -import { IsBoolean, IsNotEmpty, IsNumber, IsOptional, IsString } from 'class-validator'; +import { IsBoolean, IsNotEmpty, IsOptional, IsString } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; @@ -27,12 +27,9 @@ export class CreateConnectionDto { @ApiProperty() @IsBoolean() @IsOptional() - @IsNotEmpty({ message: 'please provide valid autoAcceptConnection' }) + @IsNotEmpty({ message: 'autoAcceptConnection should boolean' }) autoAcceptConnection: boolean; - @ApiProperty() - @IsNumber() - @IsNotEmpty({ message: 'please provide orgId' }) orgId: number; } diff --git a/apps/api-gateway/src/credential-definition/credential-definition.controller.ts b/apps/api-gateway/src/credential-definition/credential-definition.controller.ts index be8e25a0f..cbc1f1f4f 100644 --- a/apps/api-gateway/src/credential-definition/credential-definition.controller.ts +++ b/apps/api-gateway/src/credential-definition/credential-definition.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Logger, Post, Body, UseGuards, Get, Query, HttpStatus, Res } from '@nestjs/common'; +import { Controller, Logger, Post, Body, UseGuards, Get, Query, HttpStatus, Res, Param, UseFilters } 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'; @@ -15,80 +15,95 @@ 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'; +import { CustomExceptionFilter } from 'apps/api-gateway/common/exception-handler'; @ApiBearerAuth() -@UseGuards(AuthGuard('jwt'), OrgRolesGuard) -@Roles(OrgRoles.OWNER, OrgRoles.SUPER_ADMIN, OrgRoles.ADMIN, OrgRoles.ISSUER) @ApiTags('credential-definitions') - +@Controller() @ApiUnauthorizedResponse({ status: 401, description: 'Unauthorized', type: UnauthorizedErrorDto }) @ApiForbiddenResponse({ status: 403, description: 'Forbidden', type: ForbiddenErrorDto }) -@Controller('credential-definitions') +@UseFilters(CustomExceptionFilter) export class CredentialDefinitionController { constructor(private readonly credentialDefinitionService: CredentialDefinitionService) { } private readonly logger = new Logger('CredentialDefinitionController'); - @Post('/') + @Get('/orgs/:orgId/cred-defs/:credDefId') @ApiOperation({ - summary: 'Sends a credential definition to the ledger', - description: 'Create and sends a credential definition to the ledger.' + summary: 'Get an existing credential definition by Id', + description: 'Get an existing credential definition by Id' }) @ApiResponse({ status: 201, description: 'Success', type: ApiResponseDto }) - async createCredentialDefinition( - @User() user: IUserRequestInterface, - @Body() credDef: CreateCredentialDefinitionDto, + @Roles(OrgRoles.OWNER, OrgRoles.ADMIN, OrgRoles.ISSUER, OrgRoles.VERIFIER, OrgRoles.MEMBER) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + async getCredentialDefinitionById( + @Param('orgId') orgId: number, + @Param('credDefId') credentialDefinitionId: string, @Res() res: Response ): Promise { - const credentialsDefinitionDetails = await this.credentialDefinitionService.createCredentialDefinition(credDef, user); + const credentialsDefinitionDetails = await this.credentialDefinitionService.getCredentialDefinitionById(credentialDefinitionId, orgId); const credDefResponse: IResponseType = { - statusCode: HttpStatus.CREATED, - message: ResponseMessages.credentialDefinition.success.create, + statusCode: HttpStatus.OK, + message: ResponseMessages.credentialDefinition.success.fetch, data: credentialsDefinitionDetails.response }; return res.status(HttpStatus.OK).json(credDefResponse); } - @Get('/id') + + @Get('/verifiation/cred-defs/:schemaId') @ApiOperation({ - summary: 'Get an existing credential definition by Id', - description: 'Get an existing credential definition by Id' + summary: 'Get an existing credential definitions by schema Id', + description: 'Get an existing credential definitions by schema 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, + @ApiResponse({ status: 200, description: 'Success', type: ApiResponseDto }) + @UseGuards(AuthGuard('jwt')) + async getCredentialDefinitionBySchemaId( + @Param('schemaId') schemaId: string, @Res() res: Response ): Promise { - const credentialsDefinitionDetails = await this.credentialDefinitionService.getCredentialDefinitionById(credentialDefinitionId, orgId); + const credentialsDefinitions = await this.credentialDefinitionService.getCredentialDefinitionBySchemaId(schemaId); const credDefResponse: IResponseType = { statusCode: HttpStatus.OK, message: ResponseMessages.credentialDefinition.success.fetch, - data: credentialsDefinitionDetails.response + data: credentialsDefinitions.response }; return res.status(HttpStatus.OK).json(credDefResponse); } - @Get('/') + @Get('/orgs/:orgId/cred-defs') @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.' }) + @ApiQuery( + { name: 'pageNumber', required: false } + ) + @ApiQuery( + { name: 'searchByText', required: false } + ) + @ApiQuery( + { name: 'pageSize', required: false } + ) + @ApiQuery( + { name: 'sorting', required: false } + ) + @ApiQuery( + { name: 'sortByValue', required: false } + ) + @ApiQuery( + { name: 'revocable', required: false } + ) + @Roles(OrgRoles.OWNER, OrgRoles.ADMIN, OrgRoles.ISSUER, OrgRoles.VERIFIER, OrgRoles.MEMBER) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) async getAllCredDefs( + @Param('orgId') orgId: number, @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, + getAllCredDefs, user, orgId ); @@ -99,4 +114,29 @@ export class CredentialDefinitionController { }; return res.status(HttpStatus.OK).json(credDefResponse); } + + @Post('/orgs/:orgId/cred-defs') + @ApiOperation({ + summary: 'Sends a credential definition to ledger', + description: 'Sends a credential definition to ledger' + }) + @ApiResponse({ status: 201, description: 'Success', type: ApiResponseDto }) + @Roles(OrgRoles.OWNER, OrgRoles.ADMIN) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + async createCredentialDefinition( + @User() user: IUserRequestInterface, + @Body() credDef: CreateCredentialDefinitionDto, + @Param('orgId') orgId: number, + @Res() res: Response + ): Promise { + + credDef.orgId = orgId; + 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.CREATED).json(credDefResponse); + } } \ No newline at end of file diff --git a/apps/api-gateway/src/credential-definition/credential-definition.service.ts b/apps/api-gateway/src/credential-definition/credential-definition.service.ts index 36f1dbf91..799bbed7f 100644 --- a/apps/api-gateway/src/credential-definition/credential-definition.service.ts +++ b/apps/api-gateway/src/credential-definition/credential-definition.service.ts @@ -1,5 +1,5 @@ import { Injectable, Inject } from '@nestjs/common'; -import { ClientProxy, RpcException } from '@nestjs/microservices'; +import { ClientProxy } from '@nestjs/microservices'; import { CreateCredentialDefinitionDto } from './dto/create-cred-defs.dto'; import { BaseService } from '../../../../libs/service/base.service'; import { IUserRequestInterface } from '../interfaces/IUserRequestInterface'; @@ -15,32 +15,22 @@ export class CredentialDefinitionService extends BaseService { } 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); - - } + const payload = { credDef, user }; + return this.sendNats(this.credDefServiceProxy, 'create-credential-definition', payload); } 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); - - } + const payload = { credentialDefinitionId, orgId }; + return this.sendNats(this.credDefServiceProxy, 'get-credential-definition-by-id', payload); } 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); + const payload = { credDefSearchCriteria, user, orgId }; + return this.sendNats(this.credDefServiceProxy, 'get-all-credential-definitions', payload); + } - } + getCredentialDefinitionBySchemaId(schemaId: string): Promise<{ response: object }> { + const payload = { schemaId }; + return this.sendNats(this.credDefServiceProxy, 'get-all-credential-definitions-by-schema-id', payload); } } 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 index 0754ebfd8..3fa2024d6 100644 --- 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 @@ -1,4 +1,4 @@ -import { IsBoolean, IsDefined, IsNotEmpty, IsNumber, IsOptional, IsString } from 'class-validator'; +import { IsBoolean, IsDefined, IsNotEmpty, IsOptional, IsString } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; @@ -13,10 +13,6 @@ export class CreateCredentialDefinitionDto { @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 }) 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 index 76172f182..0de995ad7 100644 --- 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 @@ -2,32 +2,27 @@ /* 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'; +import { Type } from 'class-transformer'; +import { 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 }) @@ -37,11 +32,5 @@ export class GetAllCredDefsDto { @ApiProperty({ required: false }) @IsOptional() revocable: boolean = true; - - @ApiProperty({ required: true }) - @Type(() => Number) - @IsNumber() - @IsNotEmpty() - orgId: number; } diff --git a/apps/api-gateway/src/dtos/create-schema.dto.ts b/apps/api-gateway/src/dtos/create-schema.dto.ts index 810228959..bedcf23a3 100644 --- a/apps/api-gateway/src/dtos/create-schema.dto.ts +++ b/apps/api-gateway/src/dtos/create-schema.dto.ts @@ -1,10 +1,19 @@ -import { IsArray, IsNotEmpty, IsNumber, IsOptional, IsString } from 'class-validator'; +import { IsArray, IsNotEmpty, IsOptional, IsString } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; -interface IAttributeValue{ +class AttributeValue { + + @IsString() + @IsNotEmpty({ message: 'attributeName is required.' }) attributeName: string; + + @IsString() + @IsNotEmpty({ message: 'schemaDataType is required.' }) schemaDataType: string; + + @IsString() + @IsNotEmpty({ message: 'displayName is required.' }) displayName: string; } @@ -17,14 +26,19 @@ export class CreateSchemaDto { @IsString({ message: 'schema name must be a string' }) @IsNotEmpty({ message: 'please provide valid schema name' }) schemaName: string; - @ApiProperty() + @ApiProperty({ + 'example': [ + { + attributeName: 'name', + schemaDataType: 'string', + displayName: 'Name' + } + ] + }) @IsArray({ message: 'attributes must be an array' }) @IsNotEmpty({ message: 'please provide valid attributes' }) - attributes: IAttributeValue[]; + attributes: AttributeValue[]; - @ApiProperty() - @IsNumber() - @IsNotEmpty({ message: 'please provide orgId' }) orgId: number; @ApiProperty() diff --git a/apps/api-gateway/src/dtos/email-validator.dto.ts b/apps/api-gateway/src/dtos/email-validator.dto.ts new file mode 100644 index 000000000..c41ad0ef5 --- /dev/null +++ b/apps/api-gateway/src/dtos/email-validator.dto.ts @@ -0,0 +1,12 @@ + +import { ApiProperty } from '@nestjs/swagger'; +import { IsEmail, IsNotEmpty, MaxLength } from 'class-validator'; + +export class EmailValidator { + + @ApiProperty() + @IsNotEmpty({ message: 'Email is required.' }) + @MaxLength(256, { message: 'Email must be at most 256 character.' }) + @IsEmail() + email: string; +} \ No newline at end of file diff --git a/apps/api-gateway/src/dtos/fido-user.dto.ts b/apps/api-gateway/src/dtos/fido-user.dto.ts index a0864cc04..54635b27f 100644 --- a/apps/api-gateway/src/dtos/fido-user.dto.ts +++ b/apps/api-gateway/src/dtos/fido-user.dto.ts @@ -1,11 +1,7 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsArray, IsBoolean, IsEmail, IsNotEmpty, IsOptional, IsString, ValidateNested } from 'class-validator'; +import { IsArray, IsBoolean, 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' }) diff --git a/apps/api-gateway/src/ecosystem/dtos/accept-reject-invitations.dto.ts b/apps/api-gateway/src/ecosystem/dtos/accept-reject-invitations.dto.ts new file mode 100644 index 000000000..ee70759c4 --- /dev/null +++ b/apps/api-gateway/src/ecosystem/dtos/accept-reject-invitations.dto.ts @@ -0,0 +1,36 @@ +import { IsEnum, IsNotEmpty, IsString, MaxLength, MinLength} 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 AcceptRejectEcosystemInvitationDto { + + ecosystemId: string; + invitationId: string; + orgId: 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: 'organization did is required.' }) + @IsString({ message: 'organization did must be in string format.' }) + orgDid: string; + + @ApiProperty({ + enum: [Invitation.ACCEPTED, Invitation.REJECTED] + }) + @Transform(({ value }) => trim(value)) + @IsNotEmpty({ message: 'Please provide valid status' }) + @IsEnum(Invitation) + status: Invitation.ACCEPTED | Invitation.REJECTED; + +} \ No newline at end of file diff --git a/apps/api-gateway/src/ecosystem/dtos/create-ecosystem-dto.ts b/apps/api-gateway/src/ecosystem/dtos/create-ecosystem-dto.ts new file mode 100644 index 000000000..1dc36955c --- /dev/null +++ b/apps/api-gateway/src/ecosystem/dtos/create-ecosystem-dto.ts @@ -0,0 +1,58 @@ +import { ApiExtraModels, ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IsInt, IsNotEmpty, IsOptional, IsString, MaxLength, MinLength } from 'class-validator'; + +import { Transform } from 'class-transformer'; +import { trim } from '@credebl/common/cast.helper'; + +@ApiExtraModels() +export class CreateEcosystemDto { + + @ApiProperty() + @Transform(({ value }) => trim(value)) + @IsNotEmpty({ message: 'ecosystem name is required.' }) + @MinLength(2, { message: 'ecosystem name must be at least 2 characters.' }) + @MaxLength(50, { message: 'ecosystem name must be at most 50 characters.' }) + @IsString({ message: 'ecosystem name must be in string format.' }) + 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.' }) + @IsOptional() + description?: string; + + @ApiPropertyOptional() + @IsOptional() + @Transform(({ value }) => trim(value)) + @IsString({ message: 'tag must be in string format.' }) + tags?: string; + + @ApiPropertyOptional() + @IsInt({ message: 'UserId must be in number format.' }) + userId: number; + + @ApiPropertyOptional() + @IsOptional() + @Transform(({ value }) => trim(value)) + @IsString({ message: 'logo must be in string format.' }) + logo?: 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: 'organization did is required.' }) + @IsString({ message: 'organization did must be in string format.' }) + orgDid: string; + + orgId?: string; + } + \ No newline at end of file diff --git a/apps/api-gateway/src/ecosystem/dtos/decline-endorsement-transaction.dto.ts b/apps/api-gateway/src/ecosystem/dtos/decline-endorsement-transaction.dto.ts new file mode 100644 index 000000000..558736027 --- /dev/null +++ b/apps/api-gateway/src/ecosystem/dtos/decline-endorsement-transaction.dto.ts @@ -0,0 +1,6 @@ +export class DeclienEndorsementTransactionDto { + ecosystemId: string; + orgId: string; + endorsementId: string; + +} \ No newline at end of file diff --git a/apps/api-gateway/src/ecosystem/dtos/delete-invitations.dto.ts b/apps/api-gateway/src/ecosystem/dtos/delete-invitations.dto.ts new file mode 100644 index 000000000..2e849232d --- /dev/null +++ b/apps/api-gateway/src/ecosystem/dtos/delete-invitations.dto.ts @@ -0,0 +1,19 @@ +import { ApiExtraModels, ApiProperty } from '@nestjs/swagger'; +import { IsEmail, IsNotEmpty, IsString } from 'class-validator'; +import { Transform } from 'class-transformer'; +import { trim } from '@credebl/common/cast.helper'; + +@ApiExtraModels() +export class deleteEcosystemInvitationsDto { + @ApiProperty({ example: 'acqx@getnada.com' }) + @IsEmail() + @Transform(({ value }) => trim(value)) + @IsNotEmpty({ message: 'Please provide valid email' }) + @IsString({ message: 'email should be string' }) + email: string; + + ecosystemId: string; + invitationId: string; + orgId: string; + } + \ No newline at end of file diff --git a/apps/api-gateway/src/ecosystem/dtos/edit-ecosystem-dto.ts b/apps/api-gateway/src/ecosystem/dtos/edit-ecosystem-dto.ts new file mode 100644 index 000000000..96be3ba09 --- /dev/null +++ b/apps/api-gateway/src/ecosystem/dtos/edit-ecosystem-dto.ts @@ -0,0 +1,38 @@ +import { ApiExtraModels, 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 EditEcosystemDto { + @ApiPropertyOptional() + @Transform(({ value }) => trim(value)) + @IsOptional() + @IsNotEmpty({ message: 'Ecosystem name is required.' }) + @MinLength(2, { message: 'Ecosystem name must be at least 2 characters.' }) + @MaxLength(50, { message: 'Ecosystem name must be at most 50 characters.' }) + @IsString({ message: 'Ecosystem name must be in string format.' }) + name?: string; + + @ApiPropertyOptional() + @Transform(({ value }) => trim(value)) + @IsOptional() + @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: 'tag must be in string format.' }) + tags?: string; + + @ApiPropertyOptional() + @IsOptional() + @Transform(({ value }) => trim(value)) + @IsString({ message: 'logo must be in string format.' }) + logo?: string; + } + \ No newline at end of file diff --git a/apps/api-gateway/src/ecosystem/dtos/get-all-endorsements.dto.ts b/apps/api-gateway/src/ecosystem/dtos/get-all-endorsements.dto.ts new file mode 100644 index 000000000..a0878bf15 --- /dev/null +++ b/apps/api-gateway/src/ecosystem/dtos/get-all-endorsements.dto.ts @@ -0,0 +1,35 @@ + +import { Transform, Type } from 'class-transformer'; +import { toNumber } from '@credebl/common/cast.helper'; + +import { ApiProperty } from '@nestjs/swagger'; +import { IsEnum, IsOptional, IsString } from 'class-validator'; +import { EndorserTransactionType } from '@credebl/enum/enum'; + +export class GetAllEndorsementsDto { + @ApiProperty({ required: false, default: 1 }) + @IsOptional() + @Type(() => Number) + @Transform(({ value }) => toNumber(value)) + pageNumber = 1; + + @ApiProperty({ required: false }) + @IsOptional() + @IsString() + @Type(() => String) + search = ''; + + @ApiProperty({ required: false }) + @IsOptional() + @Type(() => Number) + @Transform(({ value }) => toNumber(value)) + pageSize = 10; + + @ApiProperty({ + enum: [EndorserTransactionType.SCHEMA, EndorserTransactionType.CREDENTIAL_DEFINITION] + }) + @IsOptional() + @IsEnum(EndorserTransactionType) + type: EndorserTransactionType.SCHEMA | EndorserTransactionType.CREDENTIAL_DEFINITION; + +} diff --git a/apps/api-gateway/src/ecosystem/dtos/get-all-received-invitations.dto.ts b/apps/api-gateway/src/ecosystem/dtos/get-all-received-invitations.dto.ts new file mode 100644 index 000000000..ea3fe3176 --- /dev/null +++ b/apps/api-gateway/src/ecosystem/dtos/get-all-received-invitations.dto.ts @@ -0,0 +1,31 @@ +import { Transform, Type } from 'class-transformer'; +import { toNumber } from '@credebl/common/cast.helper'; +import { ApiProperty } from '@nestjs/swagger'; +import { IsOptional, IsString } from 'class-validator'; +import { Invitation } from '@credebl/enum/enum'; + +export class GetAllSentEcosystemInvitationsDto { + + + @ApiProperty({ required: false }) + @IsOptional() + @Type(() => String) + search = ''; + + @ApiProperty({ required: false }) + @IsOptional() + @Type(() => Number) + @Transform(({ value }) => toNumber(value)) + pageNumber = 1; + + @ApiProperty({ required: false }) + @IsOptional() + @Type(() => Number) + @Transform(({ value }) => toNumber(value)) + pageSize = 10; + + @ApiProperty({ required: false }) + @IsOptional() + @IsString() + status = Invitation.PENDING; +} diff --git a/apps/api-gateway/src/ecosystem/dtos/get-all-sent-invitations.dto.ts b/apps/api-gateway/src/ecosystem/dtos/get-all-sent-invitations.dto.ts new file mode 100644 index 000000000..23c7fecd6 --- /dev/null +++ b/apps/api-gateway/src/ecosystem/dtos/get-all-sent-invitations.dto.ts @@ -0,0 +1,25 @@ +import { Transform, Type } from 'class-transformer'; +import { toNumber } from '@credebl/common/cast.helper'; + +import { ApiProperty } from '@nestjs/swagger'; +import { IsOptional } from 'class-validator'; + +export class GetAllEcosystemInvitationsDto { + @ApiProperty({ required: false, default: 1 }) + @IsOptional() + @Type(() => Number) + @Transform(({ value }) => toNumber(value)) + pageNumber = 1; + + @ApiProperty({ required: false }) + @IsOptional() + @Type(() => String) + search = ''; + + @ApiProperty({ required: false }) + @IsOptional() + @Type(() => Number) + @Transform(({ value }) => toNumber(value)) + pageSize = 10; + +} diff --git a/apps/api-gateway/src/ecosystem/dtos/get-members.dto.ts b/apps/api-gateway/src/ecosystem/dtos/get-members.dto.ts new file mode 100644 index 000000000..0644d4455 --- /dev/null +++ b/apps/api-gateway/src/ecosystem/dtos/get-members.dto.ts @@ -0,0 +1,27 @@ +import { Transform, Type } from 'class-transformer'; +import { toNumber } from '@credebl/common/cast.helper'; + +import { ApiProperty } from '@nestjs/swagger'; +import { IsOptional } from 'class-validator'; + +export class GetAllEcosystemMembersDto { + + @ApiProperty({ required: false }) + @IsOptional() + @Type(() => Number) + @Transform(({ value }) => toNumber(value)) + pageNumber = 1; + + @ApiProperty({ required: false }) + @IsOptional() + @Type(() => Number) + @Transform(({ value }) => toNumber(value)) + pageSize = 10; + + + @ApiProperty({ required: false }) + @IsOptional() + @Type(() => String) + search = ''; + +} diff --git a/apps/api-gateway/src/ecosystem/dtos/request-schema.dto.ts b/apps/api-gateway/src/ecosystem/dtos/request-schema.dto.ts new file mode 100644 index 000000000..202857f57 --- /dev/null +++ b/apps/api-gateway/src/ecosystem/dtos/request-schema.dto.ts @@ -0,0 +1,88 @@ +import { ApiExtraModels, ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { IsArray, IsBoolean, IsNotEmpty, IsOptional, IsString, ValidateNested } from 'class-validator'; +@ApiExtraModels() + +class AttributeValue { + + @IsString() + @IsNotEmpty({ message: 'attributeName is required.' }) + attributeName: string; + + @IsString() + @IsNotEmpty({ message: 'schemaDataType is required.' }) + schemaDataType: string; + + @IsString() + @IsNotEmpty({ message: 'displayName is required.' }) + displayName: string; +} + + +export class RequestSchemaDto { + @ApiProperty() + @IsString({ message: 'name must be in string format.' }) + name: string; + + @ApiProperty() + @IsString({ message: 'version must be in string format.' }) + version: string; + + @ApiProperty({ + 'example': [ + { + attributeName: 'name', + schemaDataType: 'string', + displayName: 'Name' + } + ] + }) + @IsArray({ message: 'attributes must be an array' }) + @IsNotEmpty({ message: 'please provide valid attributes' }) + attributes: AttributeValue[]; + + @ApiProperty() + @IsBoolean({ message: 'endorse must be a boolean.' }) + @IsOptional() + endorse?: boolean; + +} + +export class SchemaDetails { + @ApiProperty() + @IsString({ message: 'name must be a string.' }) + name: string; + + @ApiProperty() + @IsString({ message: 'version must be a string.' }) + version: string; + + @ApiProperty({ + example: ['name', 'id'] + }) + @IsArray({ message: 'attributes must be an array.' }) + @IsNotEmpty({ message: 'please provide valid attributes.' }) + attributes: string[]; + +} + +export class RequestCredDefDto { + @ApiProperty() + @IsBoolean({ message: 'endorse must be a boolean.' }) + @IsOptional() + endorse?: boolean; + + @ApiProperty() + @IsString({ message: 'tag must be a string.' }) + tag: string; + + @ApiProperty() + @IsString({ message: 'schemaId must be a string.' }) + schemaId: string; + + @ApiProperty() + @ValidateNested() + @IsOptional() + @Type(() => SchemaDetails) + schemaDetails?: SchemaDetails; +} \ No newline at end of file diff --git a/apps/api-gateway/src/ecosystem/dtos/send-invitation.dto.ts b/apps/api-gateway/src/ecosystem/dtos/send-invitation.dto.ts new file mode 100644 index 000000000..a9246e7bc --- /dev/null +++ b/apps/api-gateway/src/ecosystem/dtos/send-invitation.dto.ts @@ -0,0 +1,28 @@ +import { ApiExtraModels, ApiProperty } from '@nestjs/swagger'; +import { IsArray, IsEmail, IsNotEmpty, IsString, ValidateNested } from 'class-validator'; +import { Transform, Type } from 'class-transformer'; + +import { trim } from '@credebl/common/cast.helper'; + +@ApiExtraModels() +export class EcosystemInvitationDto { + + @ApiProperty({ example: 'acqx@getnada.com' }) + @IsEmail() + @Transform(({ value }) => trim(value)) + @IsNotEmpty({ message: 'Please provide valid email' }) + @IsString({ message: 'email should be string' }) + email: string; + +} + +@ApiExtraModels() +export class BulkEcosystemInvitationDto { + + @ApiProperty({ type: [EcosystemInvitationDto] }) + @IsArray() + @ValidateNested({ each: true }) + @Type(() => EcosystemInvitationDto) + invitations: EcosystemInvitationDto[]; + ecosystemId: string; +} \ No newline at end of file diff --git a/apps/api-gateway/src/ecosystem/ecosystem.controller.ts b/apps/api-gateway/src/ecosystem/ecosystem.controller.ts new file mode 100644 index 000000000..f8212a7fe --- /dev/null +++ b/apps/api-gateway/src/ecosystem/ecosystem.controller.ts @@ -0,0 +1,499 @@ +import { ApiBearerAuth, ApiForbiddenResponse, ApiOperation, ApiQuery, ApiResponse, ApiTags, ApiUnauthorizedResponse } from '@nestjs/swagger'; +import { Controller, UseFilters, Put, Param, UseGuards, Query, BadRequestException, Delete } from '@nestjs/common'; +import { EcosystemService } from './ecosystem.service'; +import { Post, Get } from '@nestjs/common'; +import { Body } from '@nestjs/common'; +import { Res } from '@nestjs/common'; +import { RequestCredDefDto, RequestSchemaDto } from './dtos/request-schema.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 { ResponseMessages } from '@credebl/common/response-messages'; +import { CustomExceptionFilter } from 'apps/api-gateway/common/exception-handler'; +import { EditEcosystemDto } from './dtos/edit-ecosystem-dto'; +import { AuthGuard } from '@nestjs/passport'; +import { GetAllSentEcosystemInvitationsDto } from './dtos/get-all-received-invitations.dto'; +import { EcosystemRoles, Invitation } from '@credebl/enum/enum'; +import { User } from '../authz/decorators/user.decorator'; +import { BulkEcosystemInvitationDto } from './dtos/send-invitation.dto'; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import { user } from '@prisma/client'; +import { AcceptRejectEcosystemInvitationDto } from './dtos/accept-reject-invitations.dto'; +import { GetAllEcosystemInvitationsDto } from './dtos/get-all-sent-invitations.dto'; +import { EcosystemRolesGuard } from '../authz/guards/ecosystem-roles.guard'; +import { EcosystemsRoles, Roles } from '../authz/decorators/roles.decorator'; +import { OrgRolesGuard } from '../authz/guards/org-roles.guard'; +import { OrgRoles } from 'libs/org-roles/enums'; +import { GetAllEcosystemMembersDto } from './dtos/get-members.dto'; +import { GetAllEndorsementsDto } from './dtos/get-all-endorsements.dto'; +import { CreateEcosystemDto } from './dtos/create-ecosystem-dto'; + + +@UseFilters(CustomExceptionFilter) +@Controller('ecosystem') +@ApiTags('ecosystem') +@ApiUnauthorizedResponse({ status: 401, description: 'Unauthorized', type: UnauthorizedErrorDto }) +@ApiForbiddenResponse({ status: 403, description: 'Forbidden', type: ForbiddenErrorDto }) +export class EcosystemController { + constructor( + private readonly ecosystemService: EcosystemService + ) { } + + @Get('/:ecosystemId/:orgId/endorsement-transactions') + @ApiOperation({ summary: 'Get all endorsement transactions', description: 'Get all endorsement transactions' }) + @ApiResponse({ status: 200, description: 'Success', type: ApiResponseDto }) + @UseGuards(AuthGuard('jwt'), EcosystemRolesGuard, OrgRolesGuard) + @ApiBearerAuth() + @EcosystemsRoles(EcosystemRoles.ECOSYSTEM_OWNER, EcosystemRoles.ECOSYSTEM_LEAD, EcosystemRoles.ECOSYSTEM_MEMBER) + @Roles(OrgRoles.OWNER, OrgRoles.ADMIN, OrgRoles.ISSUER, OrgRoles.VERIFIER, OrgRoles.MEMBER) + @ApiQuery({ + name: 'pageNumber', + type: Number, + required: false + }) + @ApiQuery({ + name: 'pageSize', + type: Number, + required: false + }) + @ApiQuery({ + name: 'search', + type: String, + required: false + }) + async getEndorsementTranasactions( + @Param('ecosystemId') ecosystemId: string, + @Param('orgId') orgId: string, + @Query() getAllEndorsementsDto: GetAllEndorsementsDto, + @Res() res: Response + ): Promise { + + const ecosystemList = await this.ecosystemService.getEndorsementTranasactions(ecosystemId, orgId, getAllEndorsementsDto); + const finalResponse: IResponseType = { + statusCode: HttpStatus.OK, + message: ResponseMessages.ecosystem.success.fetchEndorsors, + data: ecosystemList.response + }; + return res.status(HttpStatus.OK).json(finalResponse); + } + + @Get('/:orgId') + @ApiOperation({ summary: 'Get all organization ecosystems', description: 'Get all existing ecosystems of an specific organization' }) + @ApiResponse({ status: 200, description: 'Success', type: ApiResponseDto }) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + @Roles(OrgRoles.OWNER, OrgRoles.ADMIN) + @ApiBearerAuth() + async getEcosystem( + @Param('orgId') orgId: string, + @Res() res: Response + ): Promise { + const ecosystemList = await this.ecosystemService.getAllEcosystem(orgId); + const finalResponse: IResponseType = { + statusCode: HttpStatus.OK, + message: ResponseMessages.ecosystem.success.fetch, + data: ecosystemList.response + }; + return res.status(HttpStatus.OK).json(finalResponse); + } + + @Get('/:ecosystemId/:orgId/dashboard') + @ApiOperation({ summary: 'Get ecosystem dashboard details', description: 'Get ecosystem dashboard details' }) + @ApiResponse({ status: 200, description: 'Success', type: ApiResponseDto }) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard, EcosystemRolesGuard) + @Roles(OrgRoles.OWNER, OrgRoles.ADMIN) + @EcosystemsRoles(EcosystemRoles.ECOSYSTEM_OWNER, EcosystemRoles.ECOSYSTEM_LEAD, EcosystemRoles.ECOSYSTEM_MEMBER) + @ApiBearerAuth() + async getEcosystemDashboardDetails(@Param('ecosystemId') ecosystemId: string, @Param('orgId') orgId: string, @Res() res: Response): Promise { + + const getEcosystemDetails = await this.ecosystemService.getEcosystemDashboardDetails(ecosystemId, orgId); + const finalResponse: IResponseType = { + statusCode: HttpStatus.OK, + message: ResponseMessages.ecosystem.success.getEcosystemDashboard, + data: getEcosystemDetails.response + }; + return res.status(HttpStatus.OK).json(finalResponse); + + } + + @Get('/:orgId/users/invitations') + @ApiOperation({ summary: 'Get received ecosystem invitations', description: 'Get received ecosystem invitations' }) + @ApiResponse({ status: 200, description: 'Success', type: ApiResponseDto }) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + @Roles(OrgRoles.OWNER, OrgRoles.ADMIN) + @ApiBearerAuth() + @ApiQuery({ + name: 'pageNumber', + type: Number, + required: false + }) + @ApiQuery({ + name: 'pageSize', + type: Number, + required: false + }) + @ApiQuery({ + name: 'search', + type: String, + required: false + }) + @ApiQuery({ + name: 'status', + type: String, + required: false + }) + async getEcosystemInvitations( + @Query() getAllInvitationsDto: GetAllSentEcosystemInvitationsDto, + @Param('orgId') orgId: string, + @User() user: user, @Res() res: Response): Promise { + + if (!Object.values(Invitation).includes(getAllInvitationsDto.status)) { + throw new BadRequestException(ResponseMessages.ecosystem.error.invalidInvitationStatus); + } + const getEcosystemInvitation = await this.ecosystemService.getEcosystemInvitations(getAllInvitationsDto, user.email, getAllInvitationsDto.status); + const finalResponse: IResponseType = { + statusCode: HttpStatus.OK, + message: ResponseMessages.ecosystem.success.getInvitation, + data: getEcosystemInvitation.response + }; + return res.status(HttpStatus.OK).json(finalResponse); + + } + + @Get('/:ecosystemId/:orgId/invitations') + @ApiOperation({ summary: 'Get all sent invitations', description: 'Get all sent invitations' }) + @ApiResponse({ status: 200, description: 'Success', type: ApiResponseDto }) + @UseGuards(AuthGuard('jwt'), EcosystemRolesGuard, OrgRolesGuard) + @ApiBearerAuth() + @EcosystemsRoles(EcosystemRoles.ECOSYSTEM_OWNER, EcosystemRoles.ECOSYSTEM_LEAD) + @Roles(OrgRoles.OWNER, OrgRoles.ADMIN) + @ApiQuery({ + name: 'pageNumber', + type: Number, + required: false + }) + @ApiQuery({ + name: 'pageSize', + type: Number, + required: false + }) + @ApiQuery({ + name: 'search', + type: String, + required: false + }) + async getInvitationsByEcosystemId( + @Param('ecosystemId') ecosystemId: string, + @Param('orgId') orgId: string, + @Query() getAllInvitationsDto: GetAllEcosystemInvitationsDto, + @User() user: user, + @Res() res: Response): Promise { + + const getInvitationById = await this.ecosystemService.getInvitationsByEcosystemId(ecosystemId, getAllInvitationsDto, String(user.id)); + + const finalResponse: IResponseType = { + statusCode: HttpStatus.OK, + message: ResponseMessages.organisation.success.getInvitation, + data: getInvitationById.response + }; + return res.status(HttpStatus.OK).json(finalResponse); + + } + + /** + * + * @param res + * @returns Ecosystem members list + */ + @Get('/:ecosystemId/:orgId/members') + @Roles(OrgRoles.OWNER, OrgRoles.ADMIN) + @EcosystemsRoles(EcosystemRoles.ECOSYSTEM_OWNER, EcosystemRoles.ECOSYSTEM_LEAD, EcosystemRoles.ECOSYSTEM_MEMBER) + @ApiBearerAuth() + @UseGuards(AuthGuard('jwt'), EcosystemRolesGuard, OrgRolesGuard) + @ApiResponse({ status: 200, description: 'Success', type: ApiResponseDto }) + @ApiOperation({ summary: 'Get ecosystem members list', description: 'Get ecosystem members list.' }) + @ApiQuery({ + name: 'pageNumber', + type: Number, + required: false + }) + @ApiQuery({ + name: 'pageSize', + type: Number, + required: false + }) + @ApiQuery({ + name: 'search', + type: String, + required: false + }) + async getEcosystemMembers( + @Param('ecosystemId') ecosystemId: string, + @Param('orgId') orgId: string, + @Query() getEcosystemMembers: GetAllEcosystemMembersDto, + @Res() res: Response): Promise { + const members = await this.ecosystemService.getEcosystemMembers(ecosystemId, getEcosystemMembers); + const finalResponse: IResponseType = { + statusCode: HttpStatus.OK, + message: ResponseMessages.ecosystem.success.fetchMembers, + data: members?.response + }; + + return res.status(HttpStatus.OK).json(finalResponse); + } + + /** + * + * @param createOrgDto + * @param res + * @returns Ecosystem create response + */ + @Post('/:orgId') + @ApiOperation({ summary: 'Create a new ecosystem', description: 'Create an ecosystem' }) + @ApiResponse({ status: 201, description: 'Success', type: ApiResponseDto }) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + @ApiBearerAuth() + @Roles(OrgRoles.OWNER) + async createNewEcosystem( + @Body() createOrgDto: CreateEcosystemDto, + @Param('orgId') orgId: string, + @Res() res: Response): Promise { + createOrgDto.orgId = orgId; + await this.ecosystemService.createEcosystem(createOrgDto); + const finalResponse: IResponseType = { + statusCode: HttpStatus.CREATED, + message: ResponseMessages.ecosystem.success.create + }; + return res.status(HttpStatus.CREATED).json(finalResponse); + } + + @Post('/:ecosystemId/:orgId/transaction/schema') + @ApiOperation({ summary: 'Request new schema', description: 'Request new schema' }) + @ApiResponse({ status: 201, description: 'Success', type: ApiResponseDto }) + @UseGuards(AuthGuard('jwt'), EcosystemRolesGuard, OrgRolesGuard) + @ApiBearerAuth() + @EcosystemsRoles(EcosystemRoles.ECOSYSTEM_MEMBER) + @Roles(OrgRoles.OWNER, OrgRoles.ADMIN, OrgRoles.ISSUER) + async requestSchemaTransaction(@Body() requestSchemaPayload: RequestSchemaDto, @Param('orgId') orgId: number, @Param('ecosystemId') ecosystemId: string, @Res() res: Response): Promise { + await this.ecosystemService.schemaEndorsementRequest(requestSchemaPayload, orgId, ecosystemId); + const finalResponse: IResponseType = { + statusCode: HttpStatus.CREATED, + message: ResponseMessages.ecosystem.success.schemaRequest + }; + return res.status(HttpStatus.CREATED).json(finalResponse); + } + + + @Post('/:ecosystemId/:orgId/transaction/cred-def') + @ApiOperation({ summary: 'Request new credential-definition', description: 'Request new credential-definition' }) + @ApiResponse({ status: 201, description: 'Success', type: ApiResponseDto }) + @UseGuards(AuthGuard('jwt'), EcosystemRolesGuard, OrgRolesGuard) + @ApiBearerAuth() + @EcosystemsRoles(EcosystemRoles.ECOSYSTEM_MEMBER) + @Roles(OrgRoles.OWNER, OrgRoles.ADMIN, OrgRoles.ISSUER) + async requestCredDefTransaction(@Body() requestCredDefPayload: RequestCredDefDto, @Param('orgId') orgId: number, @Param('ecosystemId') ecosystemId: string, @Res() res: Response): Promise { + await this.ecosystemService.credDefEndorsementRequest(requestCredDefPayload, orgId, ecosystemId); + const finalResponse: IResponseType = { + statusCode: HttpStatus.CREATED, + message: ResponseMessages.ecosystem.success.credDefRequest + }; + return res.status(HttpStatus.CREATED).json(finalResponse); + } + + @Post('/:ecosystemId/:orgId/transaction/sign/:endorsementId') + @ApiOperation({ summary: 'Sign transaction', description: 'Sign transaction' }) + @ApiResponse({ status: 201, description: 'Success', type: ApiResponseDto }) + @UseGuards(AuthGuard('jwt'), EcosystemRolesGuard, OrgRolesGuard) + @ApiBearerAuth() + @EcosystemsRoles(EcosystemRoles.ECOSYSTEM_LEAD) + @Roles(OrgRoles.OWNER, OrgRoles.ADMIN) + async SignEndorsementRequests(@Param('endorsementId') endorsementId: string, @Param('ecosystemId') ecosystemId: string, @Param('orgId') orgId: number, @Res() res: Response): Promise { + await this.ecosystemService.signTransaction(endorsementId, ecosystemId); + const finalResponse: IResponseType = { + statusCode: HttpStatus.CREATED, + message: ResponseMessages.ecosystem.success.sign + }; + return res.status(HttpStatus.CREATED).json(finalResponse); + } + + @Post('/:ecosystemId/:orgId/transaction/sumbit/:endorsementId') + @ApiOperation({ summary: 'Sumbit transaction', description: 'Sumbit transaction' }) + @ApiResponse({ status: 201, description: 'Success', type: ApiResponseDto }) + @UseGuards(AuthGuard('jwt'), EcosystemRolesGuard, OrgRolesGuard) + @ApiBearerAuth() + @EcosystemsRoles(EcosystemRoles.ECOSYSTEM_MEMBER) + @Roles(OrgRoles.OWNER, OrgRoles.ADMIN, OrgRoles.ISSUER) + async SumbitEndorsementRequests(@Param('endorsementId') endorsementId: string, @Param('ecosystemId') ecosystemId: string, @Param('orgId') orgId: number, @Res() res: Response): Promise { + await this.ecosystemService.submitTransaction(endorsementId, ecosystemId); + const finalResponse: IResponseType = { + statusCode: HttpStatus.CREATED, + message: ResponseMessages.ecosystem.success.submit + }; + return res.status(HttpStatus.CREATED).json(finalResponse); + } + /** + * + * @param bulkInvitationDto + * @param ecosystemId + * @param user + * @param res + * @returns Ecosystem invitation send details + */ + @Post('/:ecosystemId/:orgId/invitations') + @ApiOperation({ + summary: 'Send ecosystem invitation', + description: 'Send ecosystem invitation' + }) + @ApiResponse({ status: 201, description: 'Success', type: ApiResponseDto }) + @UseGuards(AuthGuard('jwt'), EcosystemRolesGuard, OrgRolesGuard) + @ApiBearerAuth() + @EcosystemsRoles(EcosystemRoles.ECOSYSTEM_OWNER, EcosystemRoles.ECOSYSTEM_LEAD) + @Roles(OrgRoles.OWNER, OrgRoles.ADMIN) + async createInvitation(@Body() bulkInvitationDto: BulkEcosystemInvitationDto, + @Param('ecosystemId') ecosystemId: string, + @Param('orgId') orgId: string, + @User() user: user, @Res() res: Response): Promise { + + bulkInvitationDto.ecosystemId = ecosystemId; + await this.ecosystemService.createInvitation(bulkInvitationDto, String(user.id), user.email); + + const finalResponse: IResponseType = { + statusCode: HttpStatus.CREATED, + message: ResponseMessages.ecosystem.success.createInvitation + }; + + return res.status(HttpStatus.CREATED).json(finalResponse); + + } + + /** + * + * @param res + * @returns + */ + @Put('transaction/endorsement/auto') + @ApiOperation({ + summary: 'Auto sign and submit transactions', + description: 'Auto sign and submit transactions' + }) + @UseGuards(AuthGuard('jwt'), EcosystemRolesGuard, OrgRolesGuard) + @ApiBearerAuth() + @EcosystemsRoles(EcosystemRoles.ECOSYSTEM_LEAD) + @Roles(OrgRoles.OWNER, OrgRoles.ADMIN) + async autoSignAndSubmitTransaction( + @Res() res: Response + ): Promise { + await this.ecosystemService.autoSignAndSubmitTransaction(); + const finalResponse: IResponseType = { + statusCode: HttpStatus.OK, + message: ResponseMessages.ecosystem.success.AutoEndorsementTransaction + }; + return res.status(HttpStatus.OK).json(finalResponse); + } + + /** + * + * @param acceptRejectEcosystemInvitation + * @param reqUser + * @param res + * @returns Ecosystem invitation status + */ + @Put('/:orgId/invitations/:invitationId') + @ApiOperation({ + summary: 'Accept or reject ecosystem invitation', + description: 'Accept or Reject ecosystem invitations' + }) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + @ApiBearerAuth() + @Roles(OrgRoles.OWNER) + async acceptRejectEcosystemInvitaion(@Body() acceptRejectEcosystemInvitation: AcceptRejectEcosystemInvitationDto, @Param('orgId') orgId: string, @Param('invitationId') invitationId: string, @User() user: user, @Res() res: Response): Promise { + acceptRejectEcosystemInvitation.orgId = orgId; + acceptRejectEcosystemInvitation.invitationId = invitationId; + + const invitationRes = await this.ecosystemService.acceptRejectEcosystemInvitaion(acceptRejectEcosystemInvitation, user.email); + + const finalResponse: IResponseType = { + statusCode: HttpStatus.CREATED, + message: invitationRes.response + }; + return res.status(HttpStatus.CREATED).json(finalResponse); + } + + + @Put('/:ecosystemId/:orgId') + @ApiOperation({ summary: 'Edit ecosystem', description: 'Edit existing ecosystem' }) + @ApiResponse({ status: 200, description: 'Success', type: ApiResponseDto }) + @UseGuards(AuthGuard('jwt')) + @ApiBearerAuth() + @EcosystemsRoles(EcosystemRoles.ECOSYSTEM_OWNER, EcosystemRoles.ECOSYSTEM_LEAD) + @Roles(OrgRoles.OWNER) + async editEcosystem( + @Body() editEcosystemDto: EditEcosystemDto, + @Param('ecosystemId') ecosystemId: string, + @Param('orgId') orgId: string, + @Res() res: Response): Promise { + await this.ecosystemService.editEcosystem(editEcosystemDto, ecosystemId); + const finalResponse: IResponseType = { + statusCode: HttpStatus.OK, + message: ResponseMessages.ecosystem.success.update + }; + return res.status(HttpStatus.OK).json(finalResponse); + } + + + /** + * + * @param declineEndorsementTransactionRequest + * + * @param res + * @returns endorsement transaction status + */ + @Put('/:ecosystemId/:orgId/transactions/:endorsementId') + @ApiOperation({ + summary: 'Decline Endorsement Request By Lead', + description: 'Decline Endorsement Request By Lead' + }) + @UseGuards(AuthGuard('jwt'), EcosystemRolesGuard, OrgRolesGuard) + @ApiBearerAuth() + @EcosystemsRoles(EcosystemRoles.ECOSYSTEM_LEAD) + @Roles(OrgRoles.OWNER, OrgRoles.ADMIN) + async declineEndorsementRequestByLead( + @Param('ecosystemId') ecosystemId: string, + @Param('endorsementId') endorsementId: string, + @Param('orgId') orgId: string, + @Res() res: Response + ): Promise { + await this.ecosystemService.declineEndorsementRequestByLead(ecosystemId, endorsementId, orgId); + const finalResponse: IResponseType = { + statusCode: HttpStatus.OK, + message: ResponseMessages.ecosystem.success.DeclineEndorsementTransaction + }; + return res.status(HttpStatus.OK).json(finalResponse); + } + + + @Delete('/:ecosystemId/:orgId/invitations/:invitationId') + @ApiOperation({ summary: 'Delete ecosystem pending invitations', description: 'Delete ecosystem pending invitations' }) + @ApiResponse({ status: 200, description: 'Success', type: ApiResponseDto }) + @UseGuards(AuthGuard('jwt'), EcosystemRolesGuard, OrgRolesGuard) + @EcosystemsRoles(EcosystemRoles.ECOSYSTEM_OWNER, EcosystemRoles.ECOSYSTEM_LEAD) + @Roles(OrgRoles.OWNER) + @ApiBearerAuth() + async deleteEcosystemInvitations( + @Param('ecosystemId') ecosystemId: string, + @Param('invitationId') invitationId: string, + @Param('orgId') orgId: string, + @Res() res: Response): Promise { + + await this.ecosystemService.deleteEcosystemInvitations(invitationId); + const finalResponse: IResponseType = { + statusCode: HttpStatus.OK, + message: ResponseMessages.ecosystem.success.delete + }; + return res.status(HttpStatus.OK).json(finalResponse); + } + + +} \ No newline at end of file diff --git a/apps/api-gateway/src/ecosystem/ecosystem.module.ts b/apps/api-gateway/src/ecosystem/ecosystem.module.ts new file mode 100644 index 000000000..24a59ec00 --- /dev/null +++ b/apps/api-gateway/src/ecosystem/ecosystem.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 { EcosystemController } from './ecosystem.controller'; +import { EcosystemService } from './ecosystem.service'; + +@Module({ + imports: [ + HttpModule, + ConfigModule.forRoot(), + ClientsModule.register([ + { + name: 'NATS_CLIENT', + transport: Transport.NATS, + options: { + servers: [`${process.env.NATS_URL}`] + } + }, + CommonModule + ]) + ], + controllers: [EcosystemController], + providers: [EcosystemService, CommonService] +}) +export class EcosystemModule { } + diff --git a/apps/api-gateway/src/ecosystem/ecosystem.service.ts b/apps/api-gateway/src/ecosystem/ecosystem.service.ts new file mode 100644 index 000000000..3a7e6b2e7 --- /dev/null +++ b/apps/api-gateway/src/ecosystem/ecosystem.service.ts @@ -0,0 +1,180 @@ +import { Inject } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; +import { ClientProxy } from '@nestjs/microservices'; +import { BaseService } from 'libs/service/base.service'; +import { BulkEcosystemInvitationDto } from './dtos/send-invitation.dto'; +import { AcceptRejectEcosystemInvitationDto } from './dtos/accept-reject-invitations.dto'; +import { GetAllEcosystemInvitationsDto } from './dtos/get-all-sent-invitations.dto'; +import { GetAllSentEcosystemInvitationsDto } from './dtos/get-all-received-invitations.dto'; +import { GetAllEcosystemMembersDto } from './dtos/get-members.dto'; +import { GetAllEndorsementsDto } from './dtos/get-all-endorsements.dto'; + +import { RequestSchemaDto, RequestCredDefDto } from './dtos/request-schema.dto'; +import { CreateEcosystemDto } from './dtos/create-ecosystem-dto'; +import { EditEcosystemDto } from './dtos/edit-ecosystem-dto'; + +@Injectable() +export class EcosystemService extends BaseService { + constructor(@Inject('NATS_CLIENT') private readonly serviceProxy: ClientProxy) { + super('EcosystemService'); + } + + /** + * + * @param createEcosystemDto + * @returns Ecosystem creation success + */ + async createEcosystem(createEcosystemDto: CreateEcosystemDto): Promise { + const payload = { createEcosystemDto }; + return this.sendNats(this.serviceProxy, 'create-ecosystem', payload); + } + + /** + * + * @param editEcosystemDto + * @returns Ecosystem creation success + */ + async editEcosystem(editEcosystemDto: EditEcosystemDto, ecosystemId:string): Promise { + const payload = { editEcosystemDto, ecosystemId }; + return this.sendNats(this.serviceProxy, 'edit-ecosystem', payload); + } + + /** + * + * + * @returns Get all ecosystems + */ + async getAllEcosystem(orgId: string): Promise<{ response: object }> { + const payload = { orgId }; + return this.sendNats(this.serviceProxy, 'get-all-ecosystem', payload); + } + + /** + * + * + * @returns Get ecosystems dashboard card counts + */ + async getEcosystemDashboardDetails(ecosystemId: string, orgId: string): Promise<{ response: object }> { + const payload = { ecosystemId, orgId }; + return this.sendNats(this.serviceProxy, 'get-ecosystem-dashboard-details', payload); + } + + + /** + * + * @param bulkInvitationDto + * @param userId + * @returns + */ + async createInvitation(bulkInvitationDto: BulkEcosystemInvitationDto, userId: string, userEmail: string): Promise { + const payload = { bulkInvitationDto, userId, userEmail }; + return this.sendNats(this.serviceProxy, 'send-ecosystem-invitation', payload); + } + + async getInvitationsByEcosystemId( + ecosystemId: string, + getAllInvitationsDto: GetAllEcosystemInvitationsDto, + userId: string + ): Promise<{ response: object }> { + const { pageNumber, pageSize, search } = getAllInvitationsDto; + const payload = { ecosystemId, pageNumber, pageSize, search, userId }; + return this.sendNats(this.serviceProxy, 'get-sent-invitations-ecosystemId', payload); + } + + /** + * + * @returns Ecosystem members + */ + async getEcosystemMembers( + ecosystemId: string, + getEcosystemMembers: GetAllEcosystemMembersDto + ): Promise<{ response: object }> { + const { pageNumber, pageSize, search } = getEcosystemMembers; + const payload = { ecosystemId, pageNumber, pageSize, search }; + return this.sendNats(this.serviceProxy, 'fetch-ecosystem-members', payload); + } + + /** + * + * @returns Ecosystem Invitations details + */ + async getEcosystemInvitations( + getAllInvitationsDto: GetAllSentEcosystemInvitationsDto, + userEmail: string, + status: string + ): Promise<{ response: object }> { + const { pageNumber, pageSize, search } = getAllInvitationsDto; + const payload = { userEmail, status, pageNumber, pageSize, search }; + return this.sendNats(this.serviceProxy, 'get-ecosystem-invitations', payload); + } + + + async deleteEcosystemInvitations( + invitationId: string + ): Promise { + const payload = { invitationId }; + return this.sendNats(this.serviceProxy, 'delete-ecosystem-invitations', payload); + } + async acceptRejectEcosystemInvitaion( + acceptRejectInvitation: AcceptRejectEcosystemInvitationDto, + userEmail: string + ): Promise<{ response: string }> { + const payload = { acceptRejectInvitation, userEmail }; + return this.sendNats(this.serviceProxy, 'accept-reject-ecosystem-invitations', payload); + } + + + async fetchEcosystemOrg( + ecosystemId: string, + orgId: string + ): Promise<{ response: object }> { + const payload = { ecosystemId, orgId }; + return this.sendNats(this.serviceProxy, 'fetch-ecosystem-org-data', payload); + } + + async getEndorsementTranasactions( + ecosystemId: string, + orgId: string, + getAllEndorsements: GetAllEndorsementsDto + ): Promise<{ response: object }> { + const { pageNumber, pageSize, search, type } = getAllEndorsements; + const payload = { ecosystemId, orgId, pageNumber, pageSize, search, type }; + return this.sendNats(this.serviceProxy, 'get-endorsement-transactions', payload); + } + + + async schemaEndorsementRequest(requestSchemaPayload: RequestSchemaDto, orgId: number, ecosystemId: string): Promise { + const payload = { requestSchemaPayload, orgId, ecosystemId }; + return this.sendNats(this.serviceProxy, 'schema-endorsement-request', payload); + } + + async credDefEndorsementRequest(requestCredDefPayload: RequestCredDefDto, orgId: number, ecosystemId: string): Promise { + const payload = { requestCredDefPayload, orgId, ecosystemId }; + return this.sendNats(this.serviceProxy, 'credDef-endorsement-request', payload); + } + + async signTransaction(endorsementId: string, ecosystemId: string): Promise { + const payload = { endorsementId, ecosystemId }; + return this.sendNats(this.serviceProxy, 'sign-endorsement-transaction', payload); + } + + async submitTransaction(endorsementId: string, ecosystemId: string): Promise { + const payload = { endorsementId, ecosystemId }; + return this.sendNats(this.serviceProxy, 'sumbit-endorsement-transaction', payload); + } + + async autoSignAndSubmitTransaction(): Promise<{ response: object }> { + const payload = {}; + return this.sendNats(this.serviceProxy, 'auto-endorsement-transaction', payload); + } + + async declineEndorsementRequestByLead( + ecosystemId: string, + endorsementId: string, + orgId: string + ): Promise<{ response: object }> { + const payload = { ecosystemId, endorsementId, orgId }; + return this.sendNats(this.serviceProxy, 'decline-endorsement-transaction', payload); + } + +} diff --git a/apps/api-gateway/src/enum.ts b/apps/api-gateway/src/enum.ts index 0cfdecf91..7dd84939b 100644 --- a/apps/api-gateway/src/enum.ts +++ b/apps/api-gateway/src/enum.ts @@ -115,4 +115,4 @@ export enum ExpiredSubscriptionSortBy { startDate = 'startDate', endDate = 'endDate', id = 'id', -} \ No newline at end of file +} diff --git a/apps/api-gateway/src/fido/fido.controller.ts b/apps/api-gateway/src/fido/fido.controller.ts index 88b4d0fdd..ee3f066d8 100644 --- a/apps/api-gateway/src/fido/fido.controller.ts +++ b/apps/api-gateway/src/fido/fido.controller.ts @@ -1,5 +1,5 @@ -import { Body, Controller, Delete, Get, Logger, Param, Post, Put, Query, Request, Res, UseFilters } from '@nestjs/common'; -import { ApiBadRequestResponse, ApiBearerAuth, ApiForbiddenResponse, ApiOperation, ApiQuery, ApiResponse, ApiTags, ApiUnauthorizedResponse } from '@nestjs/swagger'; +import { Body, Controller, Delete, Get, HttpStatus, Logger, Param, Post, Put, Query, Request, Res, UseFilters } from '@nestjs/common'; +import { ApiBadRequestResponse, ApiBearerAuth, ApiExcludeEndpoint, 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'; @@ -8,7 +8,6 @@ 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'; @@ -16,7 +15,7 @@ import { OrgRoles } from 'libs/org-roles/enums'; import { CustomExceptionFilter } from 'apps/api-gateway/common/exception-handler'; @UseFilters(CustomExceptionFilter) -@Controller('fido') +@Controller('auth') @ApiTags('fido') @ApiUnauthorizedResponse({ status: 401, description: 'Unauthorized', type: UnauthorizedErrorDto }) @ApiForbiddenResponse({ status: 403, description: 'Forbidden', type: ForbiddenErrorDto }) @@ -24,48 +23,80 @@ import { CustomExceptionFilter } from 'apps/api-gateway/common/exception-handler export class FidoController { private logger = new Logger('FidoController'); constructor(private readonly fidoService: FidoService) { } + /** * - * @param GenerateRegistrationDto + * @param userName * @param res - * @returns Generate registration response + * @returns User get success */ - @Post('/generate-registration-options') + @Get('/passkey/:email') + @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: 'Generate registration option' }) + + @ApiOperation({ summary: 'Fetch fido user details' }) @ApiResponse({ status: 201, description: 'Success', type: ApiResponseDto }) - async generateRegistrationOption(@Body() body: GenerateRegistrationDto, @Res() res: Response): Promise { + @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('email') email: string, @Res() res: Response): Promise { try { - const { userName, deviceFlag } = body; - const registrationOption = await this.fidoService.generateRegistrationOption(userName, deviceFlag); - + const fidoUserDetails = await this.fidoService.fetchFidoUserDetails(req.params.email); const finalResponse: IResponseType = { statusCode: HttpStatus.OK, - message: ResponseMessages.fido.success.RegistrationOption, - data: registrationOption.response + message: ResponseMessages.user.success.fetchUsers, + data: fidoUserDetails.response }; return res.status(HttpStatus.OK).json(finalResponse); + } catch (error) { this.logger.error(`Error::${error}`); throw error; } } - + /** + * + * @param GenerateRegistrationDto + * @param res + * @returns Generate registration response + */ + @Post('/passkey/generate-registration/:email') + @ApiResponse({ status: 201, description: 'Success', type: ApiResponseDto }) + @ApiOperation({ summary: 'Generate registration option' }) + @ApiResponse({ status: 201, description: 'Success', type: ApiResponseDto }) + async generateRegistrationOption(@Body() body: GenerateRegistrationDto, @Param('email') email: string, @Res() res: Response): Promise { + try { + const { deviceFlag } = body; + const registrationOption = await this.fidoService.generateRegistrationOption(deviceFlag, email); + const finalResponse: IResponseType = { + statusCode: HttpStatus.CREATED, + message: ResponseMessages.fido.success.RegistrationOption, + data: registrationOption.response + }; + return res.status(HttpStatus.CREATED).json(finalResponse); + } catch (error) { + this.logger.error(`Error::${error}`); + } + } + + + /** * * @param VerifyRegistrationDto * @param res * @returns User create success */ - @Post('/verify-registration/:userName') - @ApiResponse({ status: 201, description: 'Success', type: ApiResponseDto }) + @Post('/passkey/verify-registration/:email') + @ApiResponse({ status: 200, 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); + async verifyRegistration(@Request() req, @Body() verifyRegistrationDto: VerifyRegistrationDto, @Param('email') email: string, @Res() res: Response): Promise { + const verifyRegistration = await this.fidoService.verifyRegistration(verifyRegistrationDto, req.params.email); const finalResponse: IResponseType = { statusCode: HttpStatus.OK, message: ResponseMessages.fido.success.verifyRegistration, @@ -74,32 +105,13 @@ export class FidoController { 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') + @Post('/passkey/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 { @@ -119,11 +131,10 @@ export class FidoController { * @param res * @returns Verify authentication object */ - @Post('/verify-authentication/:userName') + @Post('/passkey/verify-authentication/:email') @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); - + async verifyAuthentication(@Request() req, @Body() verifyAuthenticationDto: VerifyAuthenticationDto, @Param('email') email: string, @Res() res: Response): Promise { + const verifyAuthentication = await this.fidoService.verifyAuthentication(verifyAuthenticationDto, req.params.email); const finalResponse: IResponseType = { statusCode: HttpStatus.OK, message: ResponseMessages.fido.success.login, @@ -132,13 +143,28 @@ export class FidoController { return res.status(HttpStatus.OK).json(finalResponse); } - /** - * - * @param userName - * @param res - * @returns User get success - */ - @Get('/user-details/:userName') +/** + * + * @param updateFidoUserDetailsDto + * @param res + * @returns User update success + */ + @Put('/passkey/user-details/:credentialId') + @ApiExcludeEndpoint() + @ApiResponse({ status: 200, description: 'Success', type: ApiResponseDto }) + @ApiOperation({ summary: 'Update fido user details' }) + async updateFidoUser(@Request() req, @Body() updateFidoUserDetailsDto: UpdateFidoUserDetailsDto, @Param('credentialId') credentialId: string, @Res() res: Response): Promise { + const verifyRegistration = await this.fidoService.updateFidoUser(updateFidoUserDetailsDto, decodeURIComponent(credentialId)); + const finalResponse: IResponseType = { + statusCode: HttpStatus.OK, + message: ResponseMessages.fido.success.updateUserDetails, + data: verifyRegistration.response + }; + return res.status(HttpStatus.OK).json(finalResponse); + } + + + @Put('/passkey/:credentialId') @Roles(OrgRoles.OWNER, OrgRoles.ADMIN, OrgRoles.HOLDER, OrgRoles.ISSUER, OrgRoles.SUPER_ADMIN, OrgRoles.SUPER_ADMIN, OrgRoles.MEMBER) @ApiBearerAuth() @ApiResponse({ @@ -146,29 +172,27 @@ export class FidoController { description: 'Internal server error', type: InternalServerErrorDto }) - - @ApiOperation({ summary: 'Fetch fido user details' }) + @ApiQuery( + { name: 'deviceName', required: true } + ) + @ApiOperation({ summary: 'Update fido user device name' }) @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 { + async updateFidoUserDeviceName(@Param('credentialId') credentialId: string, @Query('deviceName') deviceName: string, @Res() res: Response): Promise { try { - const fidoUserDetails = await this.fidoService.fetchFidoUserDetails(req.params.userName); + const updateDeviceName = await this.fidoService.updateFidoUserDeviceName(credentialId, deviceName); const finalResponse: IResponseType = { statusCode: HttpStatus.OK, - message: ResponseMessages.user.success.fetchUsers, - data: fidoUserDetails.response + message: ResponseMessages.fido.success.updateDeviceName, + data: updateDeviceName.response }; return res.status(HttpStatus.OK).json(finalResponse); - } catch (error) { this.logger.error(`Error::${error}`); throw error; } } - @Delete('/device') + @Delete('/passkey/:credentialId') @Roles(OrgRoles.OWNER, OrgRoles.ADMIN, OrgRoles.HOLDER, OrgRoles.ISSUER, OrgRoles.SUPER_ADMIN, OrgRoles.SUPER_ADMIN, OrgRoles.MEMBER) @ApiBearerAuth() @ApiResponse({ @@ -181,7 +205,7 @@ export class FidoController { ) @ApiOperation({ summary: 'Delete fido user device' }) @ApiResponse({ status: 201, description: 'Success', type: ApiResponseDto }) - async deleteFidoUserDevice(@Query('credentialId') credentialId: string, @Res() res: Response): Promise { + async deleteFidoUserDevice(@Param('credentialId') credentialId: string, @Res() res: Response): Promise { try { const deleteFidoUser = await this.fidoService.deleteFidoUserDevice(credentialId); const finalResponse: IResponseType = { @@ -197,35 +221,4 @@ export class FidoController { } } - @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.service.ts b/apps/api-gateway/src/fido/fido.service.ts index cb9e87ad0..f98659706 100644 --- a/apps/api-gateway/src/fido/fido.service.ts +++ b/apps/api-gateway/src/fido/fido.service.ts @@ -11,18 +11,18 @@ export class FidoService extends BaseService { ) { super('FidoService'); } - async generateRegistrationOption(userName: string, deviceFlag: boolean): Promise<{response: object}> { + async generateRegistrationOption(deviceFlag: boolean, email:string): Promise<{response: object}> { try { - const payload = { userName, deviceFlag }; - return this.sendNats(this.fidoServiceProxy, 'generate-registration-options', payload); + const payload = { deviceFlag, email }; + return await 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 }; + async verifyRegistration(verifyRegistrationDto: VerifyRegistrationDto, email: string): Promise<{response: object}> { + const payload = { verifyRegistrationDetails: verifyRegistrationDto, email }; return this.sendNats(this.fidoServiceProxy, 'verify-registration', payload); } @@ -31,18 +31,18 @@ export class FidoService extends BaseService { return this.sendNats(this.fidoServiceProxy, 'generate-authentication-options', payload); } - async verifyAuthentication(verifyAuthenticationDto: VerifyAuthenticationDto, userName: string): Promise<{response: object}> { - const payload = { verifyAuthenticationDetails: verifyAuthenticationDto, userName }; + async verifyAuthentication(verifyAuthenticationDto: VerifyAuthenticationDto, email: string): Promise<{response: object}> { + const payload = { verifyAuthenticationDetails: verifyAuthenticationDto, email }; return this.sendNats(this.fidoServiceProxy, 'verify-authentication', payload); } - async updateFidoUser(updateFidoUserDetailsDto: UpdateFidoUserDetailsDto) : Promise<{response: object}> { - const payload = updateFidoUserDetailsDto; + async updateFidoUser(updateFidoUserDetailsDto: UpdateFidoUserDetailsDto, credentialId: string) : Promise<{response: object}> { + const payload = {updateFidoUserDetailsDto, credentialId}; return this.sendNats(this.fidoServiceProxy, 'update-user', payload); } - async fetchFidoUserDetails(userName: string): Promise<{response: string}> { - const payload = { userName }; + async fetchFidoUserDetails(email: string): Promise<{response: string}> { + const payload = { email }; return this.sendNats(this.fidoServiceProxy, 'fetch-fido-user-details', payload); } diff --git a/apps/api-gateway/src/issuance/dtos/issuance.dto.ts b/apps/api-gateway/src/issuance/dtos/issuance.dto.ts index 5deff2558..e2f6a4c37 100644 --- a/apps/api-gateway/src/issuance/dtos/issuance.dto.ts +++ b/apps/api-gateway/src/issuance/dtos/issuance.dto.ts @@ -1,4 +1,4 @@ -import { IsArray, IsNotEmpty, IsNumber, IsOptional, IsString } from 'class-validator'; +import { IsArray, IsNotEmpty, IsOptional, IsString } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; interface attribute { @@ -29,15 +29,10 @@ export class IssueCredentialDto { @IsString({ message: 'connectionId should be string' }) connectionId: string; - @ApiProperty({ example: 'v1' }) @IsOptional() @IsNotEmpty({ message: 'Please provide valid protocol-version' }) @IsString({ message: 'protocol-version should be string' }) protocolVersion?: string; - - @ApiProperty() - @IsNumber() - @IsNotEmpty({ message: 'please provide orgId' }) orgId: number; } diff --git a/apps/api-gateway/src/issuance/issuance.controller.ts b/apps/api-gateway/src/issuance/issuance.controller.ts index f0f4d9817..ba130ad24 100644 --- a/apps/api-gateway/src/issuance/issuance.controller.ts +++ b/apps/api-gateway/src/issuance/issuance.controller.ts @@ -45,7 +45,7 @@ import { CustomExceptionFilter } from 'apps/api-gateway/common/exception-handler @Controller() @UseFilters(CustomExceptionFilter) -@ApiTags('issuances') +@ApiTags('credentials') @ApiUnauthorizedResponse({ status: 401, description: 'Unauthorized', type: UnauthorizedErrorDto }) @ApiForbiddenResponse({ status: 403, description: 'Forbidden', type: ForbiddenErrorDto }) @@ -57,25 +57,110 @@ export class IssuanceController { ) { } private readonly logger = new Logger('IssuanceController'); + /** + * Description: Get all issued credentials + * @param user + * @param threadId + * @param connectionId + * @param state + * @param orgId + * + */ + @Get('/orgs/:orgId/credentials') + @UseGuards(AuthGuard('jwt')) + @ApiBearerAuth() + @ApiOperation({ + summary: `Get all issued credentials for a specific organization`, + description: `Get all issued credentials for a specific organization` + }) + @ApiResponse({ status: 200, description: 'Success', type: ApiResponseDto }) + @ApiQuery( + { name: 'threadId', required: false } + ) + @ApiQuery( + { name: 'connectionId', required: false } + ) + @ApiQuery( + { name: 'state', enum: IssueCredential, required: false } + ) + @ApiBearerAuth() + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + @Roles(OrgRoles.OWNER, OrgRoles.ADMIN, OrgRoles.ISSUER, OrgRoles.VERIFIER, OrgRoles.MEMBER, OrgRoles.HOLDER) + async getIssueCredentials( + @User() user: IUserRequest, + @Query('threadId') threadId: string, + @Query('connectionId') connectionId: string, + @Query('state') state: string, + @Param('orgId') orgId: number, + @Res() res: Response + ): Promise { + + const getCredentialDetails = await this.issueCredentialService.getIssueCredentials(user, threadId, connectionId, state, orgId); + + const finalResponse: IResponseType = { + statusCode: HttpStatus.OK, + message: ResponseMessages.issuance.success.fetch, + data: getCredentialDetails.response + }; + return res.status(HttpStatus.OK).json(finalResponse); + } + + /** + * Description: Get all issued credentials + * @param user + * @param credentialRecordId + * @param orgId + * + */ + @Get('/orgs/:orgId/credentials/:credentialRecordId') + @ApiBearerAuth() + @ApiOperation({ + summary: `Get credential by credentialRecordId`, + description: `Get credential credentialRecordId` + }) + @ApiResponse({ status: 200, description: 'Success', type: ApiResponseDto }) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + @Roles(OrgRoles.OWNER, OrgRoles.ADMIN, OrgRoles.ISSUER, OrgRoles.VERIFIER, OrgRoles.MEMBER, OrgRoles.HOLDER) + async getIssueCredentialsbyCredentialRecordId( + @User() user: IUserRequest, + @Param('credentialRecordId') credentialRecordId: string, + @Param('orgId') orgId: number, + + @Res() res: Response + ): Promise { + + const getCredentialDetails = await this.issueCredentialService.getIssueCredentialsbyCredentialRecordId(user, credentialRecordId, orgId); + + const finalResponse: IResponseType = { + statusCode: HttpStatus.OK, + message: ResponseMessages.issuance.success.fetch, + data: getCredentialDetails.response + }; + return res.status(HttpStatus.OK).json(finalResponse); + } + /** * Description: Issuer send credential to create offer * @param user * @param issueCredentialDto */ - @Post('issue-credentials/create-offer') - @UseGuards(AuthGuard('jwt')) + @Post('/orgs/:orgId/credentials/offer') @ApiBearerAuth() @ApiOperation({ summary: `Send credential details to create-offer`, description: `Send credential details to create-offer` }) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + @Roles(OrgRoles.OWNER, OrgRoles.ADMIN, OrgRoles.ISSUER) @ApiResponse({ status: 201, description: 'Success', type: ApiResponseDto }) async sendCredential( @User() user: IUserRequest, + @Param('orgId') orgId: number, @Body() issueCredentialDto: IssueCredentialDto, @Res() res: Response ): Promise { + issueCredentialDto.orgId = orgId; const attrData = issueCredentialDto.attributes; attrData.forEach((data) => { @@ -114,6 +199,7 @@ export class IssuanceController { @Param('id') id: number, @Res() res: Response ): Promise { + this.logger.debug(`issueCredentialDto ::: ${issueCredentialDto}`); const getCredentialDetails = await this.issueCredentialService.getIssueCredentialWebhook(issueCredentialDto, id); const finalResponse: IResponseType = { statusCode: HttpStatus.CREATED, @@ -124,93 +210,4 @@ export class IssuanceController { } - /** - * 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 } - ) - @ApiBearerAuth() - @Roles(OrgRoles.OWNER, OrgRoles.ADMIN, OrgRoles.ISSUER) - @UseGuards(AuthGuard('jwt'), OrgRolesGuard) - 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.OK, - message: ResponseMessages.issuance.success.fetch, - data: getCredentialDetails.response - }; - return res.status(HttpStatus.OK).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.OK, - message: ResponseMessages.issuance.success.fetch, - data: getCredentialDetails.response - }; - return res.status(HttpStatus.OK).json(finalResponse); - } - } 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 index 53d0a082f..6a3cedf44 100644 --- a/apps/api-gateway/src/organization/dtos/get-all-organizations.dto.ts +++ b/apps/api-gateway/src/organization/dtos/get-all-organizations.dto.ts @@ -1,6 +1,6 @@ import { Transform, Type } from 'class-transformer'; // import { SortValue } from '../../enum'; -import { toNumber, trim } from '@credebl/common/cast.helper'; +import { toNumber } from '@credebl/common/cast.helper'; import { ApiProperty } from '@nestjs/swagger'; import { IsOptional } from 'class-validator'; @@ -15,7 +15,6 @@ export class GetAllOrganizationsDto { @ApiProperty({ required: false }) @IsOptional() @Type(() => String) - @Transform(({ value }) => trim(value)) search = ''; @ApiProperty({ required: false }) 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 index edc9eaf1c..eaa15380d 100644 --- 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 @@ -1,5 +1,5 @@ import { Transform, Type } from 'class-transformer'; -import { toNumber, trim } from '@credebl/common/cast.helper'; +import { toNumber } from '@credebl/common/cast.helper'; import { ApiProperty } from '@nestjs/swagger'; import { IsOptional } from 'class-validator'; @@ -14,7 +14,6 @@ export class GetAllSentInvitationsDto { @ApiProperty({ required: false }) @IsOptional() @Type(() => String) - @Transform(({ value }) => trim(value)) search = ''; @ApiProperty({ required: false }) diff --git a/apps/api-gateway/src/organization/dtos/send-invitation.dto.ts b/apps/api-gateway/src/organization/dtos/send-invitation.dto.ts index a6c1b43c8..df6b85aae 100644 --- a/apps/api-gateway/src/organization/dtos/send-invitation.dto.ts +++ b/apps/api-gateway/src/organization/dtos/send-invitation.dto.ts @@ -1,5 +1,5 @@ import { ApiExtraModels, ApiProperty } from '@nestjs/swagger'; -import { IsArray, IsEmail, IsNotEmpty, IsNumber, IsString, ValidateNested } from 'class-validator'; +import { IsArray, IsEmail, IsNotEmpty, IsString, ValidateNested } from 'class-validator'; import { Transform, Type } from 'class-transformer'; import { trim } from '@credebl/common/cast.helper'; @@ -29,9 +29,5 @@ export class BulkSendInvitationDto { @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 index 44569b38d..c3fb2d6aa 100644 --- a/apps/api-gateway/src/organization/dtos/update-organization-dto.ts +++ b/apps/api-gateway/src/organization/dtos/update-organization-dto.ts @@ -1,5 +1,5 @@ import { ApiExtraModels, ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { IsNotEmpty, IsNumber, IsOptional, IsString, MaxLength, MinLength } from 'class-validator'; +import { IsNotEmpty, IsOptional, IsString, IsBoolean, MaxLength, MinLength } from 'class-validator'; import { Transform } from 'class-transformer'; import { trim } from '@credebl/common/cast.helper'; @@ -7,9 +7,7 @@ import { trim } from '@credebl/common/cast.helper'; @ApiExtraModels() export class UpdateOrganizationDto { - @ApiProperty() - @IsNotEmpty({ message: 'orgId is required.' }) - @IsNumber() + orgId: number; @ApiProperty() @@ -37,4 +35,9 @@ export class UpdateOrganizationDto { @IsOptional() website: string; + @ApiPropertyOptional({ example: true }) + @IsBoolean({ message: 'isPublic should be boolean' }) + @IsOptional() + isPublic?: boolean = false; + } \ 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 index 5ddb6a9f8..f1270f88b 100644 --- a/apps/api-gateway/src/organization/dtos/update-user-roles.dto.ts +++ b/apps/api-gateway/src/organization/dtos/update-user-roles.dto.ts @@ -1,21 +1,11 @@ -import { IsArray, IsNotEmpty, IsNumber } from 'class-validator'; +import { IsArray, IsNotEmpty } 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() + orgId: number; userId: number; @ApiProperty({ example: [2, 1, 3] }) diff --git a/apps/api-gateway/src/organization/organization.controller.ts b/apps/api-gateway/src/organization/organization.controller.ts index 05fafc1da..a4a796895 100644 --- a/apps/api-gateway/src/organization/organization.controller.ts +++ b/apps/api-gateway/src/organization/organization.controller.ts @@ -1,4 +1,4 @@ -import { ApiBearerAuth, ApiForbiddenResponse, ApiOperation, ApiQuery, ApiResponse, ApiTags, ApiUnauthorizedResponse } from '@nestjs/swagger'; +import { ApiBearerAuth, ApiForbiddenResponse, ApiOperation, ApiParam, ApiQuery, ApiResponse, ApiTags, ApiUnauthorizedResponse } from '@nestjs/swagger'; import { CommonService } from '@credebl/common'; import { Controller, Get, Put, Param, UseGuards, UseFilters } from '@nestjs/common'; import { OrganizationService } from './organization.service'; @@ -26,9 +26,11 @@ import { GetAllOrganizationsDto } from './dtos/get-all-organizations.dto'; import { GetAllSentInvitationsDto } from './dtos/get-all-sent-invitations.dto'; import { UpdateOrganizationDto } from './dtos/update-organization-dto'; import { CustomExceptionFilter } from 'apps/api-gateway/common/exception-handler'; +import { IUserRequestInterface } from '../interfaces/IUserRequestInterface'; +import { GetAllUsersDto } from '../user/dto/get-all-users.dto'; @UseFilters(CustomExceptionFilter) -@Controller('organization') +@Controller('orgs') @ApiTags('organizations') @ApiUnauthorizedResponse({ status: 401, description: 'Unauthorized', type: UnauthorizedErrorDto }) @ApiForbiddenResponse({ status: 403, description: 'Forbidden', type: ForbiddenErrorDto }) @@ -39,39 +41,6 @@ export class OrganizationController { 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); - - } - /** * * @param user @@ -79,9 +48,9 @@ export class OrganizationController { * @param res * @returns Users list of organization */ - @Get('/public') + @Get('/public-profile') @ApiResponse({ status: 200, description: 'Success', type: ApiResponseDto }) - @ApiOperation({ summary: 'Get public organization list', description: 'Get users list.' }) + @ApiOperation({ summary: 'Get all public profile of organizations', description: 'Get all public profile of organizations.' }) @ApiQuery({ name: 'pageNumber', type: Number, @@ -109,18 +78,41 @@ export class OrganizationController { return res.status(HttpStatus.OK).json(finalResponse); } - @Get('public-profile') + @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); + + } + + @Get('public-profiles/:orgSlug') @ApiOperation({ summary: 'Fetch user details', description: 'Fetch user details' }) - @ApiQuery({ - name: 'id', - type: Number, + + @ApiParam({ + name: 'orgSlug', + type: String, required: false }) - async getPublicProfile(@User() reqUser: user, @Query('id') id: number, @Res() res: Response): Promise { - const userData = await this.organizationService.getPublicProfile(id); + async getPublicProfile(@Param('orgSlug') orgSlug: string, @Res() res: Response): Promise { + const userData = await this.organizationService.getPublicProfile(orgSlug); const finalResponse: IResponseType = { statusCode: HttpStatus.OK, @@ -132,135 +124,221 @@ export class OrganizationController { } - @Get('/roles') - @ApiOperation({ - summary: 'Fetch org-roles details', - description: 'Fetch org-roles details' - }) + + @Get('/dashboard/:orgId') + @ApiOperation({ summary: 'Get an organization', description: 'Get an organization' }) @ApiResponse({ status: 200, description: 'Success', type: ApiResponseDto }) @UseGuards(AuthGuard('jwt')) @ApiBearerAuth() - async getOrgRoles(@Res() res: Response): Promise { - const orgRoles = await this.organizationService.getOrgRoles(); + async getOrganizationDashboard(@Param('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.fetchOrgRoles, - data: orgRoles + message: ResponseMessages.organisation.success.getOrgDashboard, + data: getOrganization.response }; - return res.status(HttpStatus.OK).json(finalResponse); } - @Post('/invitations') - @ApiOperation({ - summary: 'Create organization invitation', - description: 'Create send invitation' - }) + @Get('/:orgId/invitations') + @ApiOperation({ summary: 'Get an invitations', description: 'Get an invitations' }) @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); + @ApiQuery({ + name: 'pageNumber', + type: Number, + required: false + }) + @ApiQuery({ + name: 'pageSize', + type: Number, + required: false + }) + @ApiQuery({ + name: 'search', + type: String, + required: false + }) + @Roles(OrgRoles.OWNER, OrgRoles.SUPER_ADMIN, OrgRoles.ADMIN, OrgRoles.ISSUER, OrgRoles.VERIFIER, OrgRoles.MEMBER) + async getInvitationsByOrgId(@Param('orgId') orgId: number, @Query() getAllInvitationsDto: GetAllSentInvitationsDto, @Res() res: Response): Promise { + + const getInvitationById = await this.organizationService.getInvitationsByOrgId(orgId, getAllInvitationsDto); const finalResponse: IResponseType = { - statusCode: HttpStatus.CREATED, - message: ResponseMessages.organisation.success.createInvitation + statusCode: HttpStatus.OK, + message: ResponseMessages.organisation.success.getInvitation, + data: getInvitationById.response }; - - return res.status(HttpStatus.CREATED).json(finalResponse); + return res.status(HttpStatus.OK).json(finalResponse); } - @Get('/invitations/:id') - @ApiOperation({ summary: 'Get an invitations', description: 'Get an invitations' }) + @Get('/') + @ApiOperation({ summary: 'Get all organizations', description: 'Get all organizations' }) @ApiResponse({ status: 200, description: 'Success', type: ApiResponseDto }) @UseGuards(AuthGuard('jwt')) @ApiBearerAuth() - async getInvitationsByOrgId(@Param('id') orgId: number, @Query() getAllInvitationsDto: GetAllSentInvitationsDto, @Res() res: Response): Promise { + @ApiQuery({ + name: 'pageNumber', + example: '1', + type: Number, + required: false + }) + @ApiQuery({ + name: 'pageSize', + example: '10', + type: Number, + required: false + }) + @ApiQuery({ + name: 'search', + example: '', + type: String, + required: false + }) + async getOrganizations(@Query() getAllOrgsDto: GetAllOrganizationsDto, @Res() res: Response, @User() reqUser: user): Promise { - const getInvitationById = await this.organizationService.getInvitationsByOrgId(orgId, getAllInvitationsDto); + const getOrganizations = await this.organizationService.getOrganizations(getAllOrgsDto, reqUser.id); const finalResponse: IResponseType = { statusCode: HttpStatus.OK, - message: ResponseMessages.organisation.success.getInvitation, - data: getInvitationById.response + message: ResponseMessages.organisation.success.getOrganizations, + data: getOrganizations.response }; return res.status(HttpStatus.OK).json(finalResponse); } - @Get('/dashboard') + @Get('/:orgId') @ApiOperation({ summary: 'Get an organization', description: 'Get an organization' }) @ApiResponse({ status: 200, description: 'Success', type: ApiResponseDto }) - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) @ApiBearerAuth() - @ApiQuery( - { name: 'orgId', required: true } - ) - async getOrganizationDashboard(@Query('orgId') orgId: number, @Res() res: Response, @User() reqUser: user): Promise { + @Roles(OrgRoles.OWNER, OrgRoles.ADMIN, OrgRoles.ISSUER, OrgRoles.VERIFIER, OrgRoles.MEMBER) + async getOrganization(@Param('orgId') orgId: number, @Res() res: Response, @User() reqUser: user): Promise { - const getOrganization = await this.organizationService.getOrganizationDashboard(orgId, reqUser.id); + const getOrganization = await this.organizationService.getOrganization(orgId, reqUser.id); const finalResponse: IResponseType = { statusCode: HttpStatus.OK, - message: ResponseMessages.organisation.success.getOrgDashboard, + message: ResponseMessages.organisation.success.getOrganization, data: getOrganization.response }; return res.status(HttpStatus.OK).json(finalResponse); } - - @Put('user-roles') - @Roles(OrgRoles.OWNER, OrgRoles.ADMIN) + /** + * + * @param user + * @param orgId + * @param res + * @returns Users list of organization + */ + @Get('/:orgId/users') + @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: 'Update user roles', description: 'update user roles' }) - async updateUserRoles(@Body() updateUserDto: UpdateUserRolesDto, @Res() res: Response): Promise { - - await this.organizationService.updateUserRoles(updateUserDto, updateUserDto.userId); - + @ApiOperation({ summary: 'Get organization users list', description: 'Get organization users list.' }) + @ApiQuery({ + name: 'pageNumber', + type: Number, + required: false + }) + @ApiQuery({ + name: 'pageSize', + type: Number, + required: false + }) + @ApiQuery({ + name: 'search', + type: String, + required: false + }) + async getOrganizationUsers(@User() user: IUserRequestInterface, @Query() getAllUsersDto: GetAllUsersDto, @Param('orgId') orgId: number, @Res() res: Response): Promise { + const users = await this.organizationService.getOrgUsers(orgId, getAllUsersDto); const finalResponse: IResponseType = { statusCode: HttpStatus.OK, - message: ResponseMessages.organisation.success.updateUserRoles + message: ResponseMessages.user.success.fetchUsers, + data: users?.response }; 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 }) + @Post('/') + @ApiOperation({ summary: 'Create a new Organization', description: 'Create an organization' }) + @ApiResponse({ status: 201, description: 'Success', type: ApiResponseDto }) @UseGuards(AuthGuard('jwt')) @ApiBearerAuth() - async getOrganization(@Param('id') orgId: number, @Res() res: Response, @User() reqUser: user): Promise { + 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); + } - const getOrganization = await this.organizationService.getOrganization(orgId, reqUser.id); + @Post('/:orgId/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, @Param('orgId') orgId: number, @User() user: user, @Res() res: Response): Promise { + + bulkInvitationDto.orgId = orgId; + await this.organizationService.createInvitation(bulkInvitationDto, user.id, user.email); const finalResponse: IResponseType = { - statusCode: HttpStatus.OK, - message: ResponseMessages.organisation.success.getOrganization, - data: getOrganization.response + statusCode: HttpStatus.CREATED, + message: ResponseMessages.organisation.success.createInvitation }; - return res.status(HttpStatus.OK).json(finalResponse); + + return res.status(HttpStatus.CREATED).json(finalResponse); } + @Put('/:orgId/user-roles/:userId') + @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, @Param('orgId') orgId: number, @Param('userId') userId: number, @Res() res: Response): Promise { + + updateUserDto.orgId = orgId; + updateUserDto.userId = userId; + 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); + } - @Put('/') + @Put('/:orgId') @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, @User() reqUser: user): Promise { + async updateOrganization(@Body() updateOrgDto: UpdateOrganizationDto, @Param('orgId') orgId: number, @Res() res: Response, @User() reqUser: user): Promise { - await this.organizationService.updateOrganization(updateOrgDto, reqUser.id); + updateOrgDto.orgId = orgId; + await this.organizationService.updateOrganization(updateOrgDto, reqUser.id, orgId); const finalResponse: IResponseType = { statusCode: HttpStatus.OK, @@ -268,4 +346,4 @@ export class OrganizationController { }; return res.status(HttpStatus.OK).json(finalResponse); } -} +} \ No newline at end of file diff --git a/apps/api-gateway/src/organization/organization.service.ts b/apps/api-gateway/src/organization/organization.service.ts index eb626e6d1..2af36e772 100644 --- a/apps/api-gateway/src/organization/organization.service.ts +++ b/apps/api-gateway/src/organization/organization.service.ts @@ -1,6 +1,6 @@ import { Inject } from '@nestjs/common'; import { Injectable } from '@nestjs/common'; -import { ClientProxy, RpcException } from '@nestjs/microservices'; +import { ClientProxy } 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'; @@ -8,6 +8,7 @@ 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'; +import { GetAllUsersDto } from '../user/dto/get-all-users.dto'; @Injectable() export class OrganizationService extends BaseService { @@ -21,13 +22,8 @@ export class OrganizationService extends BaseService { * @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); - } + const payload = { createOrgDto, userId }; + return this.sendNats(this.serviceProxy, 'create-organization', payload); } /** @@ -35,14 +31,9 @@ export class OrganizationService extends BaseService { * @param updateOrgDto * @returns Organization update Success */ - async updateOrganization(updateOrgDto: UpdateOrganizationDto, userId: number): Promise { - try { - const payload = { updateOrgDto, userId }; - return this.sendNats(this.serviceProxy, 'update-organization', payload); - } catch (error) { - this.logger.error(`In service Error: ${error}`); - throw new RpcException(error.response); - } + async updateOrganization(updateOrgDto: UpdateOrganizationDto, userId: number, orgId: number): Promise { + const payload = { updateOrgDto, userId, orgId }; + return this.sendNats(this.serviceProxy, 'update-organization', payload); } /** @@ -65,8 +56,8 @@ export class OrganizationService extends BaseService { return this.sendNats(this.serviceProxy, 'get-public-organizations', payload); } - async getPublicProfile(id: number): Promise<{ response: object }> { - const payload = { id }; + async getPublicProfile(orgSlug: string): Promise<{ response: object }> { + const payload = { orgSlug }; try { return this.sendNats(this.serviceProxy, 'get-organization-public-profile', payload); } catch (error) { @@ -109,13 +100,8 @@ export class OrganizationService extends BaseService { * @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); - } + const payload = {}; + return this.sendNats(this.serviceProxy, 'get-org-roles', payload); } /** @@ -123,14 +109,9 @@ export class OrganizationService extends BaseService { * @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); - } + async createInvitation(bulkInvitationDto: BulkSendInvitationDto, userId: number, userEmail: string): Promise { + const payload = { bulkInvitationDto, userId, userEmail }; + return this.sendNats(this.serviceProxy, 'send-invitation', payload); } /** @@ -143,4 +124,14 @@ export class OrganizationService extends BaseService { const payload = { orgId: updateUserDto.orgId, roleIds: updateUserDto.orgRoleId, userId }; return this.sendNats(this.serviceProxy, 'update-user-roles', payload); } + + async getOrgUsers( + 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-user', payload); + } } diff --git a/apps/api-gateway/src/platform/platform.controller.ts b/apps/api-gateway/src/platform/platform.controller.ts index 9cfc655c4..2905c9e49 100644 --- a/apps/api-gateway/src/platform/platform.controller.ts +++ b/apps/api-gateway/src/platform/platform.controller.ts @@ -1,13 +1,75 @@ -import { Controller, Logger } from '@nestjs/common'; +import { Controller, Get, HttpStatus, Logger, Query, Res, UseFilters, UseGuards } from '@nestjs/common'; import { PlatformService } from './platform.service'; -import { ApiBearerAuth } from '@nestjs/swagger'; +import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { ApiResponseDto } from '../dtos/apiResponse.dto'; +import { GetAllSchemaByPlatformDto } from '../schema/dtos/get-all-schema.dto'; +import { IUserRequestInterface } from '../interfaces/IUserRequestInterface'; +import { User } from '../authz/decorators/user.decorator'; +import { Response } from 'express'; +import { ISchemaSearchInterface } from '../interfaces/ISchemaSearch.interface'; +import IResponseType from '@credebl/common/interfaces/response.interface'; +import { ResponseMessages } from '@credebl/common/response-messages'; +import { CustomExceptionFilter } from 'apps/api-gateway/common/exception-handler'; +import { AuthGuard } from '@nestjs/passport'; @ApiBearerAuth() -@Controller('connections') +@Controller('platform') +@UseFilters(CustomExceptionFilter) export class PlatformController { constructor(private readonly platformService: PlatformService) { } private readonly logger = new Logger('PlatformController'); + @Get('/schemas') + @ApiTags('schemas') + @ApiOperation({ + summary: 'Get all schemas from platform.', + description: 'Get all schemas from platform.' + }) + @UseGuards(AuthGuard('jwt')) + @ApiResponse({ status: 200, description: 'Success', type: ApiResponseDto }) + async getAllSchema( + @Query() getAllSchemaDto: GetAllSchemaByPlatformDto, + @Res() res: Response, + @User() user: IUserRequestInterface + ): Promise { + const { pageSize, searchByText, pageNumber, sorting, sortByValue } = getAllSchemaDto; + const schemaSearchCriteria: ISchemaSearchInterface = { + pageNumber, + searchByText, + pageSize, + sorting, + sortByValue + }; + const schemasResponse = await this.platformService.getAllSchema(schemaSearchCriteria, user); + + const finalResponse: IResponseType = { + statusCode: HttpStatus.OK, + message: ResponseMessages.schema.success.fetch, + data: schemasResponse.response + }; + return res.status(HttpStatus.OK).json(finalResponse); + } + + @Get('/ledgers') + @ApiTags('ledgers') + @ApiOperation({ + summary: 'Get all ledgers from platform.', + description: 'Get all ledgers from platform.' + }) + @UseGuards(AuthGuard('jwt')) + @ApiResponse({ status: 200, description: 'Success', type: ApiResponseDto }) + async getAllLedgers( + @Res() res: Response + ): Promise { + const networksResponse = await this.platformService.getAllLedgers(); + + const finalResponse: IResponseType = { + statusCode: HttpStatus.OK, + message: ResponseMessages.ledger.success.fetch, + data: networksResponse.response + }; + return res.status(HttpStatus.OK).json(finalResponse); + } } diff --git a/apps/api-gateway/src/platform/platform.module.ts b/apps/api-gateway/src/platform/platform.module.ts index ba0ffe2ec..692fe9918 100644 --- a/apps/api-gateway/src/platform/platform.module.ts +++ b/apps/api-gateway/src/platform/platform.module.ts @@ -3,7 +3,6 @@ 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: [ @@ -11,10 +10,12 @@ import { commonNatsOptions } from 'libs/service/nats.options'; ClientsModule.register([ { name: 'NATS_CLIENT', - ...commonNatsOptions('AGENT_SERVICE:REQUESTER') + transport: Transport.NATS, + options: { + servers: [`${process.env.NATS_URL}`] + } } ]) - ], controllers: [PlatformController], providers: [PlatformService] diff --git a/apps/api-gateway/src/platform/platform.service.ts b/apps/api-gateway/src/platform/platform.service.ts index 54a3bb353..e15742fd6 100644 --- a/apps/api-gateway/src/platform/platform.service.ts +++ b/apps/api-gateway/src/platform/platform.service.ts @@ -1,10 +1,8 @@ -import { Injectable, Inject, Logger, HttpException } from '@nestjs/common'; +import { Injectable, Inject } 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'; +import { ISchemaSearchInterface } from '../interfaces/ISchemaSearch.interface'; +import { IUserRequestInterface } from '../interfaces/IUserRequestInterface'; @Injectable() export class PlatformService extends BaseService { @@ -14,52 +12,18 @@ export class PlatformService extends BaseService { super('PlatformService'); } + async getAllSchema(schemaSearchCriteria: ISchemaSearchInterface, user: IUserRequestInterface): Promise<{ + response: object; + }> { + const schemaSearch = { schemaSearchCriteria, user }; + return this.sendNats(this.platformServiceProxy, 'get-all-schemas', schemaSearch); - /** - * 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...'); + async getAllLedgers(): Promise<{ + response: object; + }> { 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); + return this.sendNats(this.platformServiceProxy, 'get-all-ledgers', payload); } } 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 index af4a49838..0004e716d 100644 --- a/apps/api-gateway/src/schema/dtos/get-all-schema.dto.ts +++ b/apps/api-gateway/src/schema/dtos/get-all-schema.dto.ts @@ -2,9 +2,8 @@ /* 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'; +import { Type } from 'class-transformer'; +import { IsOptional } from 'class-validator'; export class GetAllSchemaDto { @ApiProperty({ required: false }) @@ -14,7 +13,6 @@ export class GetAllSchemaDto { @ApiProperty({ required: false }) @IsOptional() @Type(() => String) - @Transform(({ value }) => trim(value)) searchByText: string = ''; @ApiProperty({ required: false }) @@ -23,19 +21,11 @@ export class GetAllSchemaDto { @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() - @IsOptional() - orgId?: number; } export class GetCredentialDefinitionBySchemaIdDto { @@ -56,13 +46,6 @@ export class GetCredentialDefinitionBySchemaIdDto { @ApiProperty({ required: false }) @IsOptional() sortByValue: string = SortValue.DESC; - - @ApiProperty({ required: true }) - @Type(() => Number) - @IsNumber() - @IsNotEmpty() - @IsOptional() - orgId?: number; } export class GetAllSchemaByPlatformDto { @@ -73,7 +56,6 @@ export class GetAllSchemaByPlatformDto { @ApiProperty({ required: false }) @IsOptional() @Type(() => String) - @Transform(({ value }) => trim(value)) searchByText: string = ''; @ApiProperty({ required: false }) @@ -82,7 +64,6 @@ export class GetAllSchemaByPlatformDto { @ApiProperty({ required: false }) @IsOptional() - @Transform(({ value }) => trim(value)) sorting: string = 'id'; @ApiProperty({ required: false }) diff --git a/apps/api-gateway/src/schema/schema.controller.ts b/apps/api-gateway/src/schema/schema.controller.ts index 00f4ecacc..98e829e4b 100644 --- a/apps/api-gateway/src/schema/schema.controller.ts +++ b/apps/api-gateway/src/schema/schema.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Logger, Post, Body, HttpStatus, UseGuards, Get, Query, BadRequestException, Res, UseFilters } from '@nestjs/common'; +import { Controller, Logger, Post, Body, HttpStatus, UseGuards, Get, Query, BadRequestException, Res, UseFilters, Param } from '@nestjs/common'; /* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable camelcase */ import { ApiOperation, ApiResponse, ApiTags, ApiBearerAuth, ApiForbiddenResponse, ApiUnauthorizedResponse, ApiQuery } from '@nestjs/swagger'; @@ -12,17 +12,16 @@ 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 { GetAllSchemaByPlatformDto, GetAllSchemaDto, GetCredentialDefinitionBySchemaIdDto } from './dtos/get-all-schema.dto'; +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'; -import { TransformStreamDefaultController } from 'node:stream/web'; import { CustomExceptionFilter } from 'apps/api-gateway/common/exception-handler'; @UseFilters(CustomExceptionFilter) -@Controller('schemas') +@Controller('orgs') @ApiTags('schemas') @ApiBearerAuth() @ApiUnauthorizedResponse({ status: 401, description: 'Unauthorized', type: UnauthorizedErrorDto }) @@ -32,94 +31,19 @@ export class SchemaController { ) { } private readonly logger = new Logger('SchemaController'); - @Post('/') - @ApiOperation({ - summary: 'Sends a schema to the ledger', - description: 'Create and sends a schema to the ledger.' - }) - @Roles(OrgRoles.OWNER, OrgRoles.SUPER_ADMIN, OrgRoles.ADMIN, OrgRoles.ISSUER) + @Get('/:orgId/schemas/:schemaId') + @Roles(OrgRoles.OWNER, OrgRoles.ADMIN, OrgRoles.ISSUER, OrgRoles.VERIFIER, OrgRoles.MEMBER) @UseGuards(AuthGuard('jwt'), OrgRolesGuard) - @ApiResponse({ status: 201, description: 'Success', type: ApiResponseDto }) - async createSchema(@Res() res: Response, @Body() schema: CreateSchemaDto, @User() user: IUserRequestInterface): Promise { - - schema.attributes.forEach((attribute) => { - if (attribute.hasOwnProperty('attributeName') && attribute.hasOwnProperty('schemaDataType') && attribute.hasOwnProperty('displayName')) { - if (attribute.hasOwnProperty('attributeName') && '' === attribute?.attributeName) { - throw new BadRequestException('Attribute must not be empty'); - } else if (attribute.hasOwnProperty('attributeName') && '' === attribute?.attributeName?.trim()) { - throw new BadRequestException('Attributes should not contain space'); - } else if (attribute.hasOwnProperty('schemaDataType') && '' === attribute?.schemaDataType) { - throw new BadRequestException('Schema Data Type should not contain space'); - } else if (attribute.hasOwnProperty('schemaDataType') && '' === attribute?.schemaDataType?.trim()) { - throw new BadRequestException('Schema Data Type should not contain space'); - } else if (attribute.hasOwnProperty('displayName') && '' === attribute?.displayName) { - throw new BadRequestException('Display Name Type should not contain space'); - } - } else { - throw new BadRequestException('Please provide a valid attributes'); - } - }); - 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 by org id.', - description: 'Get all schemas by org id.' + summary: 'Get schema information from the ledger using its schema ID.', + description: 'Get schema information from the ledger using its schema ID.' }) - @Roles(OrgRoles.OWNER, OrgRoles.SUPER_ADMIN, OrgRoles.ADMIN, OrgRoles.ISSUER) - @UseGuards(AuthGuard('jwt'), OrgRolesGuard) - @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') - @Roles(OrgRoles.OWNER, OrgRoles.SUPER_ADMIN, OrgRoles.ADMIN, OrgRoles.ISSUER) - @UseGuards(AuthGuard('jwt'), OrgRolesGuard) - @ApiOperation({ - summary: 'Retrieve an existing schema from the ledger using its schemaId', - description: 'Retrieve an existing schema from the ledger using its schemaId' - }) - @ApiQuery( - { name: 'schemaId', required: true } - ) - - @ApiQuery( - { name: 'orgId', required: true } - ) @ApiResponse({ status: 200, description: 'Success', type: ApiResponseDto }) async getSchemaById( @Res() res: Response, - @Query('schemaId') schemaId: string, - @Query('orgId') orgId: number + @Param('orgId') orgId: number, + @Param('schemaId') schemaId: string ): Promise { if (!schemaId) { @@ -134,20 +58,37 @@ export class SchemaController { return res.status(HttpStatus.OK).json(finalResponse); } - @Get('/credential-definitions') + @Get('/:orgId/schemas/:schemaId/cred-defs') @ApiOperation({ - summary: 'Get an existing credential definition list by schemaId', - description: 'Get an existing credential definition list by schemaId' + summary: 'Get credential definition list by schema Id', + description: 'Get credential definition list by schema Id' }) - @ApiQuery( - { name: 'schemaId', required: true } - ) - @ApiQuery( - { name: 'orgId', required: false } - ) @ApiResponse({ status: 200, description: 'Success', type: ApiResponseDto }) + @ApiQuery({ + name: 'pageNumber', + type: Number, + required: false + }) + @ApiQuery({ + name: 'pageSize', + type: Number, + required: false + }) + @ApiQuery({ + name: 'sorting', + type: String, + required: false + }) + @ApiQuery({ + name: 'sortByValue', + type: String, + required: false + }) + @Roles(OrgRoles.OWNER, OrgRoles.ADMIN, OrgRoles.ISSUER, OrgRoles.VERIFIER, OrgRoles.MEMBER) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) async getcredDeffListBySchemaId( - @Query('schemaId') schemaId: string, + @Param('orgId') orgId: number, + @Param('schemaId') schemaId: string, @Query() GetCredentialDefinitionBySchemaIdDto: GetCredentialDefinitionBySchemaIdDto, @Res() res: Response, @User() user: IUserRequestInterface): Promise { @@ -155,14 +96,8 @@ export class SchemaController { 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 credentialDefinitionList = await this.appService.getcredDeffListBySchemaId(schemaId, GetCredentialDefinitionBySchemaIdDto, user, orgId); const finalResponse: IResponseType = { statusCode: HttpStatus.OK, message: ResponseMessages.schema.success.fetch, @@ -171,17 +106,46 @@ export class SchemaController { return res.status(HttpStatus.OK).json(finalResponse); } - @Get('/platform') + @Get('/:orgId/schemas') @ApiOperation({ - summary: 'Get all schemas from platform.', - description: 'Get all schemas from platform.' + summary: 'Get all schemas by org id.', + description: 'Get all schemas by org id.' }) + @ApiQuery({ + name: 'pageNumber', + type: Number, + required: false + }) + @ApiQuery({ + name: 'searchByText', + type: String, + required: false + }) + @ApiQuery({ + name: 'pageSize', + type: Number, + required: false + }) + @ApiQuery({ + name: 'sorting', + type: String, + required: false + }) + @ApiQuery({ + name: 'sortByValue', + type: String, + required: false + }) + @Roles(OrgRoles.OWNER, OrgRoles.ADMIN, OrgRoles.ISSUER, OrgRoles.VERIFIER, OrgRoles.MEMBER) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) @ApiResponse({ status: 200, description: 'Success', type: ApiResponseDto }) - async getAllSchema( - @Query() getAllSchemaDto: GetAllSchemaByPlatformDto, + async getSchemas( + @Query() getAllSchemaDto: GetAllSchemaDto, + @Param('orgId') orgId: number, @Res() res: Response, @User() user: IUserRequestInterface ): Promise { + const { pageSize, searchByText, pageNumber, sorting, sortByValue } = getAllSchemaDto; const schemaSearchCriteria: ISchemaSearchInterface = { pageNumber, @@ -190,7 +154,7 @@ export class SchemaController { sorting, sortByValue }; - const schemasResponse = await this.appService.getAllSchema(schemaSearchCriteria, user); + const schemasResponse = await this.appService.getSchemas(schemaSearchCriteria, user, orgId); const finalResponse: IResponseType = { statusCode: HttpStatus.OK, @@ -199,4 +163,43 @@ export class SchemaController { }; return res.status(HttpStatus.OK).json(finalResponse); } + + @Post('/:orgId/schemas') + @ApiOperation({ + summary: 'Create and sends a schema to the ledger.', + description: 'Create and sends a schema to the ledger.' + }) + @Roles(OrgRoles.OWNER, OrgRoles.ADMIN) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + @ApiResponse({ status: 201, description: 'Success', type: ApiResponseDto }) + async createSchema(@Res() res: Response, @Body() schema: CreateSchemaDto, @Param('orgId') orgId: number, @User() user: IUserRequestInterface): Promise { + + schema.attributes.forEach((attribute) => { + if (attribute.hasOwnProperty('attributeName') && attribute.hasOwnProperty('schemaDataType') && attribute.hasOwnProperty('displayName')) { + if (attribute.hasOwnProperty('attributeName') && '' === attribute?.attributeName) { + throw new BadRequestException('Attribute must not be empty'); + } else if (attribute.hasOwnProperty('attributeName') && '' === attribute?.attributeName?.trim()) { + throw new BadRequestException('Attributes should not contain space'); + } else if (attribute.hasOwnProperty('schemaDataType') && '' === attribute?.schemaDataType) { + throw new BadRequestException('Schema Data Type should not contain space'); + } else if (attribute.hasOwnProperty('schemaDataType') && '' === attribute?.schemaDataType?.trim()) { + throw new BadRequestException('Schema Data Type should not contain space'); + } else if (attribute.hasOwnProperty('displayName') && '' === attribute?.displayName) { + throw new BadRequestException('Display Name Type should not contain space'); + } + } else { + throw new BadRequestException('Please provide a valid attributes'); + } + }); + + schema.orgId = orgId; + 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); + } } diff --git a/apps/api-gateway/src/schema/schema.service.ts b/apps/api-gateway/src/schema/schema.service.ts index 3a260f706..333f4b5ff 100644 --- a/apps/api-gateway/src/schema/schema.service.ts +++ b/apps/api-gateway/src/schema/schema.service.ts @@ -1,5 +1,5 @@ import { Injectable, Inject } from '@nestjs/common'; -import { ClientProxy, RpcException } from '@nestjs/microservices'; +import { ClientProxy } from '@nestjs/microservices'; import { BaseService } from '../../../../libs/service/base.service'; import { CreateSchemaDto } from '../dtos/create-schema.dto'; import { ICredDeffSchemaSearchInterface, ISchemaSearchInterface } from '../interfaces/ISchemaSearch.interface'; @@ -15,61 +15,28 @@ export class SchemaService extends BaseService { 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); - - } + const payload = { schema, user, orgId }; + return this.sendNats(this.schemaServiceProxy, 'create-schema', payload); } 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); - - } + const payload = { schemaId, orgId }; + return this.sendNats(this.schemaServiceProxy, 'get-schema-by-id', payload); } 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); - - } + const schemaSearch = { schemaSearchCriteria, user, orgId }; + return this.sendNats(this.schemaServiceProxy, 'get-schemas', schemaSearch); } 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); - - } - } - - getAllSchema(schemaSearchCriteria: ISchemaSearchInterface, user: IUserRequestInterface): Promise<{ - response: object; - }> { - try { - const schemaSearch = { schemaSearchCriteria, user }; - return this.sendNats(this.schemaServiceProxy, 'get-all-schemas', schemaSearch); - } catch (error) { - throw new RpcException(error.response); - - } + const payload = { schemaId, schemaSearchCriteria, user, orgId }; + return this.sendNats(this.schemaServiceProxy, 'get-cred-deff-list-by-schemas-id', payload); } - } \ No newline at end of file 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 index e3d40e7ea..6d16ed0d9 100644 --- a/apps/api-gateway/src/user/dto/accept-reject-invitation.dto.ts +++ b/apps/api-gateway/src/user/dto/accept-reject-invitation.dto.ts @@ -6,10 +6,6 @@ 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 }) @@ -25,4 +21,4 @@ export class AcceptRejectInvitationDto { @IsEnum(Invitation) status: Invitation.ACCEPTED | Invitation.REJECTED; -} +} \ No newline at end of file diff --git a/apps/api-gateway/src/user/dto/add-user.dto.ts b/apps/api-gateway/src/user/dto/add-user.dto.ts index a72eb45eb..e0dea93df 100644 --- a/apps/api-gateway/src/user/dto/add-user.dto.ts +++ b/apps/api-gateway/src/user/dto/add-user.dto.ts @@ -1,18 +1,25 @@ 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'; +import { IsBoolean, IsEmail, IsNotEmpty, IsOptional, IsString } from 'class-validator'; export class AddUserDetails { + + @ApiProperty({ example: 'awqx@getnada.com' }) + @IsEmail() + @IsNotEmpty({ message: 'Please provide valid email' }) + @IsString({ message: 'email should be string' }) + email: string; + @ApiProperty({ example: 'Alen' }) + @IsNotEmpty({ message: 'Please provide valid email' }) @IsString({ message: 'firstName should be string' }) - @IsOptional() - firstName?: string; + firstName: string; @ApiProperty({ example: 'Harvey' }) + @IsNotEmpty({ message: 'Please provide valid email' }) @IsString({ message: 'lastName should be string' }) - @IsOptional() - lastName?: string; + lastName: string; @ApiProperty() @Transform(({ value }) => trim(value)) 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 index 42e50d3d7..89f079f19 100644 --- a/apps/api-gateway/src/user/dto/get-all-invitations.dto.ts +++ b/apps/api-gateway/src/user/dto/get-all-invitations.dto.ts @@ -1,6 +1,6 @@ import { IsOptional, IsString } from 'class-validator'; import { Transform, Type } from 'class-transformer'; -import { toNumber, trim } from '@credebl/common/cast.helper'; +import { toNumber } from '@credebl/common/cast.helper'; import { ApiProperty } from '@nestjs/swagger'; import { Invitation } from '@credebl/enum/enum'; @@ -15,7 +15,6 @@ export class GetAllInvitationsDto { @ApiProperty({ required: false }) @IsOptional() @Type(() => String) - @Transform(({ value }) => trim(value)) search = ''; @ApiProperty({ required: false }) 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 index 47b98aeb8..8a9bbd8b2 100644 --- a/apps/api-gateway/src/user/dto/get-all-users.dto.ts +++ b/apps/api-gateway/src/user/dto/get-all-users.dto.ts @@ -1,5 +1,5 @@ import { Transform, Type } from 'class-transformer'; -import { toNumber, trim } from '@credebl/common/cast.helper'; +import { toNumber } from '@credebl/common/cast.helper'; import { ApiProperty } from '@nestjs/swagger'; import { IsOptional } from 'class-validator'; @@ -15,7 +15,6 @@ export class GetAllUsersDto { @ApiProperty({ required: false }) @IsOptional() @Type(() => String) - @Transform(({ value }) => trim(value)) search = ''; @ApiProperty({ required: false }) diff --git a/apps/api-gateway/src/user/dto/login-user.dto.ts b/apps/api-gateway/src/user/dto/login-user.dto.ts index 09c5cafaf..f62980e6e 100644 --- a/apps/api-gateway/src/user/dto/login-user.dto.ts +++ b/apps/api-gateway/src/user/dto/login-user.dto.ts @@ -1,8 +1,8 @@ -import { IsBoolean, IsEmail, IsNotEmpty, IsOptional, IsString, MaxLength, MinLength } from 'class-validator'; +import { IsBoolean, IsEmail, IsNotEmpty, IsOptional, IsString } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; import { Transform } from 'class-transformer'; -import { trim } from '@credebl/common/cast.helper'; +import { trim } from '@credebl/common/cast.helper'; export class LoginUserDto { @ApiProperty({ example: 'awqx@getnada.com' }) @@ -21,29 +21,4 @@ export class LoginUserDto { @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; -} +} \ No newline at end of file diff --git a/apps/api-gateway/src/user/dto/update-user-profile.dto.ts b/apps/api-gateway/src/user/dto/update-user-profile.dto.ts index 88d1c28b5..e4f39fef5 100644 --- a/apps/api-gateway/src/user/dto/update-user-profile.dto.ts +++ b/apps/api-gateway/src/user/dto/update-user-profile.dto.ts @@ -1,16 +1,13 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { IsNotEmpty, IsNumber, IsOptional, IsString} from 'class-validator'; +import { IsOptional, IsString, IsBoolean } from 'class-validator'; export class UpdateUserProfileDto { - @ApiProperty() - @IsNotEmpty({ message: 'userId is required.' }) - @IsNumber() id: number; @ApiPropertyOptional() @IsOptional() - @IsString({message:'ProfileLogoUrl should be string'}) + @IsString({ message: 'ProfileLogoUrl should be string' }) profileImg?: string; @ApiProperty({ example: 'Alen' }) @@ -22,4 +19,9 @@ export class UpdateUserProfileDto { @IsString({ message: 'lastName should be string' }) @IsOptional() lastName?: string; + + @ApiPropertyOptional({ example: true }) + @IsBoolean({ message: 'isPublic should be boolean' }) + @IsOptional() + isPublic?: boolean = false; } \ No newline at end of file diff --git a/apps/api-gateway/src/user/user.controller.ts b/apps/api-gateway/src/user/user.controller.ts index 0677ad8eb..b14d9e3c6 100644 --- a/apps/api-gateway/src/user/user.controller.ts +++ b/apps/api-gateway/src/user/user.controller.ts @@ -1,11 +1,10 @@ -import { Controller, Post, Put, Body, Param, UseFilters } from '@nestjs/common'; +import { Controller, Post, Put, Body, Param, UseFilters, Res, HttpStatus, BadRequestException, Get, Query, UseGuards } from '@nestjs/common'; import { UserService } from './user.service'; -import { UserEmailVerificationDto } from './dto/create-user.dto'; import { ApiBearerAuth, - ApiBody, ApiForbiddenResponse, ApiOperation, + ApiParam, ApiQuery, ApiResponse, ApiTags, @@ -14,34 +13,22 @@ import { 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 { AddPasskeyDetails, AddUserDetails } from './dto/add-user.dto'; import { UpdateUserProfileDto } from './dto/update-user-profile.dto'; import { CustomExceptionFilter } from 'apps/api-gateway/common/exception-handler'; +import { AddPasskeyDetails } from './dto/add-user.dto'; +import { EmailValidator } from '../dtos/email-validator.dto'; @UseFilters(CustomExceptionFilter) @Controller('users') @@ -51,24 +38,6 @@ import { CustomExceptionFilter } from 'apps/api-gateway/common/exception-handler 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 @@ -76,12 +45,9 @@ export class UserController { * @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) + @Get('/public-profiles') @ApiResponse({ status: 200, description: 'Success', type: ApiResponseDto }) - @ApiOperation({ summary: 'Get organization users list', description: 'Get organization users list.' }) + @ApiOperation({ summary: 'Get users list', description: 'Get users list.' }) @ApiQuery({ name: 'pageNumber', type: Number, @@ -97,10 +63,9 @@ export class UserController { type: String, required: false }) - async getOrganizationUsers(@User() user: IUserRequestInterface, @Query() getAllUsersDto: GetAllUsersDto, @Query('orgId') orgId: number, @Res() res: Response): Promise { + async get(@User() user: IUserRequestInterface, @Query() getAllUsersDto: GetAllUsersDto, @Res() res: Response): Promise { - const org = user.selectedOrg?.orgId; - const users = await this.userService.getOrgUsers(org, getAllUsersDto); + const users = await this.userService.get(getAllUsersDto); const finalResponse: IResponseType = { statusCode: HttpStatus.OK, message: ResponseMessages.user.success.fetchUsers, @@ -110,98 +75,30 @@ export class UserController { return res.status(HttpStatus.OK).json(finalResponse); } - - /** - * - * @param user - * @param orgId - * @param res - * @returns Users list of organization - */ - @Get('/public') - @ApiResponse({ status: 200, description: 'Success', type: ApiResponseDto }) - @ApiOperation({ summary: 'Get users list', description: 'Get users list.' }) - @ApiQuery({ - name: 'pageNumber', - type: Number, - required: false - }) - @ApiQuery({ - name: 'pageSize', - type: Number, - required: false + @Get('public-profiles/:username') + @ApiOperation({ + summary: 'Fetch user details', + description: 'Fetch user details' }) - @ApiQuery({ - name: 'search', + @ApiParam({ + name: 'username', type: String, required: false }) - async get(@User() user: IUserRequestInterface, @Query() getAllUsersDto: GetAllUsersDto, @Res() res: Response): Promise { - - const users = await this.userService.get(getAllUsersDto); - const finalResponse: IResponseType = { - statusCode: HttpStatus.OK, - message: ResponseMessages.user.success.fetchUsers, - data: users.response - }; - - return res.status(HttpStatus.OK).json(finalResponse); - } - + async getPublicProfile(@Param('username') username: string, @Res() res: Response): Promise { + const userData = await this.userService.getPublicProfile(username); - /** - * - * @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 + message: ResponseMessages.user.success.fetchProfile, + data: userData.response }; 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') + @Get('/profile') @ApiOperation({ summary: 'Fetch login user details', description: 'Fetch login user details' @@ -222,37 +119,56 @@ export class UserController { } - @Get('public-profile') + @Get('/activity') @ApiOperation({ - summary: 'Fetch user details', - description: 'Fetch user details' - }) - @ApiQuery({ - name: 'id', - type: Number, - required: false + summary: 'organization invitations', + description: 'Fetch organization invitations' }) - async getPublicProfile(@User() reqUser: user, @Query('id') id: number, @Res() res: Response): Promise { - const userData = await this.userService.getPublicProfile(id); + @UseGuards(AuthGuard('jwt')) + @ApiBearerAuth() + @ApiQuery({ name: 'limit', required: true }) + async getUserActivities(@Query('limit') limit: number, @Res() res: Response, @User() reqUser: user): Promise { + + const userDetails = await this.userService.getUserActivities(reqUser.id, limit); const finalResponse: IResponseType = { statusCode: HttpStatus.OK, - message: ResponseMessages.user.success.fetchProfile, - data: userData.response + message: ResponseMessages.user.success.userActivity, + data: userDetails.response }; return res.status(HttpStatus.OK).json(finalResponse); - } - @Get('invitations') + + @Get('/org-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 { + @ApiQuery({ + name: 'pageNumber', + type: Number, + required: false + }) + @ApiQuery({ + name: 'pageSize', + type: Number, + required: false + }) + @ApiQuery({ + name: 'search', + type: String, + required: false + }) + @ApiQuery({ + name: 'status', + type: String, + required: false + }) + async invitations(@Query() getAllInvitationsDto: GetAllInvitationsDto, @User() reqUser: user, @Res() res: Response): Promise { if (!Object.values(Invitation).includes(getAllInvitationsDto.status)) { throw new BadRequestException(ResponseMessages.user.error.invalidInvitationStatus); @@ -270,6 +186,26 @@ export class UserController { } + /** + * + * @param email + * @param res + * @returns User email check + */ + @Get('/:email') + @ApiOperation({ summary: 'Check user exist', description: 'check user existence' }) + async checkUserExist(@Param() emailParam: EmailValidator, @Res() res: Response): Promise { + const userDetails = await this.userService.checkUserExist(emailParam.email); + + const finalResponse: IResponseType = { + statusCode: HttpStatus.OK, + message: ResponseMessages.user.success.checkEmail, + data: userDetails.response + }; + + return res.status(HttpStatus.OK).json(finalResponse); + + } /** * @@ -278,84 +214,23 @@ export class UserController { * @param res * @returns Organization invitation status */ - @Post('invitations') + @Post('/org-invitations/:invitationId') @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 { + async acceptRejectInvitaion(@Body() acceptRejectInvitation: AcceptRejectInvitationDto, @Param('invitationId') invitationId: string, @User() reqUser: user, @Res() res: Response): Promise { + acceptRejectInvitation.invitationId = parseInt(invitationId); const invitationRes = await this.userService.acceptRejectInvitaion(acceptRejectInvitation, reqUser.id); const finalResponse: IResponseType = { - statusCode: HttpStatus.OK, + statusCode: HttpStatus.CREATED, 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 { - let finalResponse; - let userDetails; - - if (false === userInfo.isPasskey) { - - const decryptedPassword = this.commonService.decryptPassword(userInfo.password); - if (8 <= decryptedPassword.length && 50 >= decryptedPassword.length) { - this.commonService.passwordValidation(decryptedPassword); - userInfo.password = decryptedPassword; - userDetails = await this.userService.addUserDetailsInKeyCloak(email, userInfo); - finalResponse = { - statusCode: HttpStatus.CREATED, - message: ResponseMessages.user.success.create, - data: userDetails.response - }; - } else { - throw new BadRequestException('Password name must be between 8 to 50 Characters'); - } - } else { - - userDetails = await this.userService.addUserDetailsInKeyCloak(email, userInfo); - finalResponse = { - statusCode: HttpStatus.CREATED, - message: ResponseMessages.user.success.create, - data: userDetails.response - }; - } - return res.status(HttpStatus.OK).json(finalResponse); + return res.status(HttpStatus.CREATED).json(finalResponse); } @@ -367,8 +242,10 @@ export class UserController { @ApiResponse({ status: 200, description: 'Success', type: ApiResponseDto }) @ApiBearerAuth() @UseGuards(AuthGuard('jwt')) - async updateUserProfile(@Body() updateUserProfileDto: UpdateUserProfileDto, @Res() res: Response): Promise { + async updateUserProfile(@Body() updateUserProfileDto: UpdateUserProfileDto, @User() reqUser: user, @Res() res: Response): Promise { + const userId = reqUser.id; + updateUserProfileDto.id = userId; await this.userService.updateUserProfile(updateUserProfileDto); const finalResponse: IResponseType = { @@ -379,40 +256,20 @@ export class UserController { } - @Get('/activity') - @ApiOperation({ - summary: 'organization invitations', - description: 'Fetch organization invitations' - }) - @UseGuards(AuthGuard('jwt')) - @ApiBearerAuth() - @ApiQuery({ name: 'limit', required: true }) - async getUserActivities(@Query('limit') limit: number, @Res() res: Response, @User() reqUser: user): Promise { - - const userDetails = await this.userService.getUserActivities(reqUser.id, limit); - - const finalResponse: IResponseType = { - statusCode: HttpStatus.OK, - message: 'User activities fetched successfully', - data: userDetails.response - }; - - return res.status(HttpStatus.OK).json(finalResponse); - } - - @Post('/password/:email') - @ApiOperation({ summary: 'Add user information', description: 'Add user information' }) + @Put('/password/:email') + @ApiOperation({ summary: 'Store user password details', description: 'Store user password details' }) @UseGuards(AuthGuard('jwt')) @ApiBearerAuth() async addPasskey(@Body() userInfo: AddPasskeyDetails, @Param('email') email: string, @Res() res: Response): Promise { const userDetails = await this.userService.addPasskey(email, userInfo); const finalResponse = { - statusCode: HttpStatus.CREATED, - message: ResponseMessages.user.success.create, + statusCode: HttpStatus.OK, + message: ResponseMessages.user.success.update, data: userDetails.response }; return res.status(HttpStatus.OK).json(finalResponse); } + } \ No newline at end of file diff --git a/apps/api-gateway/src/user/user.service.ts b/apps/api-gateway/src/user/user.service.ts index 6dfa2b0f5..63e8ecc9e 100644 --- a/apps/api-gateway/src/user/user.service.ts +++ b/apps/api-gateway/src/user/user.service.ts @@ -1,89 +1,42 @@ import { Inject } from '@nestjs/common'; import { Injectable } from '@nestjs/common'; -import { ClientProxy, RpcException } from '@nestjs/microservices'; +import { ClientProxy } 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'; import { UpdateUserProfileDto } from './dto/update-user-profile.dto'; import { AddPasskeyDetails } from './dto/add-user.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)}`); - } + return this.sendNats(this.serviceProxy, 'get-user-profile', payload); } - async getPublicProfile(id: number): Promise<{ response: object }> { - const payload = { id }; - try { - return this.sendNats(this.serviceProxy, 'get-user-public-profile', payload); - } catch (error) { - this.logger.error(`Error in get user:${JSON.stringify(error)}`); - } + async getPublicProfile(username: string): Promise<{ response: object }> { + const payload = { username }; + return this.sendNats(this.serviceProxy, 'get-user-public-profile', payload); } async updateUserProfile(updateUserProfileDto: UpdateUserProfileDto): Promise<{ response: object }> { - const payload = {updateUserProfileDto }; - try { - return this.sendNats(this.serviceProxy, 'update-user-profile', payload); - } catch (error) { - throw new RpcException(error.response); - } + const payload = { updateUserProfileDto }; + return this.sendNats(this.serviceProxy, 'update-user-profile', payload); } - + async findUserinSupabase(id: string): Promise<{ response: object }> { const payload = { id }; - - try { - return this.sendNats(this.serviceProxy, 'get-user-by-supabase', payload); - } catch (error) { - this.logger.error(`Error in get user:${JSON.stringify(error)}`); - } + return this.sendNats(this.serviceProxy, 'get-user-by-supabase', payload); } async invitations(id: number, status: string, getAllInvitationsDto: GetAllInvitationsDto): Promise<{ response: object }> { - const {pageNumber, pageSize, search} = getAllInvitationsDto; + const { pageNumber, pageSize, search } = getAllInvitationsDto; const payload = { id, status, pageNumber, pageSize, search }; return this.sendNats(this.serviceProxy, 'get-org-invitations', payload); } @@ -96,39 +49,25 @@ export class UserService extends BaseService { return this.sendNats(this.serviceProxy, 'accept-reject-invitations', payload); } - async getOrgUsers( - 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 get( getAllUsersDto: GetAllUsersDto ): Promise<{ response: object }> { - const {pageNumber, pageSize, search} = getAllUsersDto; + const { pageNumber, pageSize, search } = getAllUsersDto; const payload = { pageNumber, pageSize, search }; return this.sendNats(this.serviceProxy, 'fetch-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); - } - async getUserActivities(userId: number, limit: number): Promise<{ response: object }> { const payload = { userId, limit }; return this.sendNats(this.serviceProxy, 'get-user-activity', payload); } - async addPasskey(userEmail: string, userInfo:AddPasskeyDetails): Promise<{ response: string }> { + async addPasskey(userEmail: string, userInfo: AddPasskeyDetails): Promise<{ response: string }> { const payload = { userEmail, userInfo }; return this.sendNats(this.serviceProxy, 'add-passkey', payload); } diff --git a/apps/api-gateway/src/verification/dto/request-proof.dto.ts b/apps/api-gateway/src/verification/dto/request-proof.dto.ts index 4a5ee350e..7081a3f58 100644 --- a/apps/api-gateway/src/verification/dto/request-proof.dto.ts +++ b/apps/api-gateway/src/verification/dto/request-proof.dto.ts @@ -1,24 +1,31 @@ -import { IsArray, IsEmail, IsNotEmpty, IsNumber, IsObject, IsOptional, IsString, MaxLength } from 'class-validator'; +import { IsArray, IsEmail, IsNotEmpty, 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'; -class IProofRequestAttribute { +export class ProofRequestAttribute { @IsString() + @IsNotEmpty({ message: 'attributeName is required.' }) attributeName: string; @IsString() + @IsNotEmpty({ message: 'schemaId is required.' }) + schemaId: string; + + @IsString() + @IsOptional() + @IsNotEmpty({ message: 'condition is required.' }) condition?: string; @IsString() + @IsOptional() + @IsNotEmpty({ message: 'value is required.' }) value?: string; @IsString() + @IsOptional() + @IsNotEmpty({ message: 'credDefId is required.' }) credDefId?: string; - - @IsString() - schemaId: string; } export class RequestProof { @@ -43,15 +50,11 @@ export class RequestProof { @IsArray({ message: 'attributes must be in array' }) @IsObject({ each: true }) @IsNotEmpty({ message: 'please provide valid attributes' }) - attributes: IProofRequestAttribute[]; + attributes: ProofRequestAttribute[]; @ApiProperty() @IsOptional() comment: string; - - @ApiProperty() - @IsNumber() - @IsNotEmpty({ message: 'please provide orgId' }) orgId: number; @IsString({ message: 'auto accept proof must be in string' }) @@ -80,7 +83,7 @@ export class OutOfBandRequestProof { @IsArray({ message: 'attributes must be in array' }) @IsObject({ each: true }) @IsNotEmpty({ message: 'please provide valid attributes' }) - attributes: IProofRequestAttribute[]; + attributes: ProofRequestAttribute[]; @ApiProperty({ example: 'string' }) @IsNotEmpty({ message: 'Please provide valid emailId' }) @@ -94,10 +97,6 @@ export class OutOfBandRequestProof { @ApiProperty() @IsOptional() comment: string; - - @ApiProperty() - @IsNumber() - @IsNotEmpty({ message: 'please provide orgId' }) orgId: number; @IsString({ message: 'autoAcceptProof must be in string' }) diff --git a/apps/api-gateway/src/verification/verification.controller.ts b/apps/api-gateway/src/verification/verification.controller.ts index 328bd2540..f9f555c52 100644 --- a/apps/api-gateway/src/verification/verification.controller.ts +++ b/apps/api-gateway/src/verification/verification.controller.ts @@ -12,7 +12,7 @@ import { ApiQuery, ApiExcludeEndpoint } from '@nestjs/swagger'; -import { Controller, Logger, Post, Body, Get, Query, HttpStatus, Res, UseGuards, Param, UseFilters } from '@nestjs/common'; +import { Controller, Logger, Post, Body, Get, Query, HttpStatus, Res, UseGuards, Param, UseFilters, BadRequestException } from '@nestjs/common'; import { ApiResponseDto } from '../dtos/apiResponse.dto'; import { UnauthorizedErrorDto } from '../dtos/unauthorized-error.dto'; import { ForbiddenErrorDto } from '../dtos/forbidden-error.dto'; @@ -33,33 +33,27 @@ import { CustomExceptionFilter } from 'apps/api-gateway/common/exception-handler @UseFilters(CustomExceptionFilter) @ApiBearerAuth() @Controller() +@ApiTags('verifications') export class VerificationController { constructor(private readonly verificationService: VerificationService) { } private readonly logger = new Logger('VerificationController'); - @Get('/proofs/form-data') - @ApiTags('verifications') + @Get('/orgs/:orgId/proofs/:proofId/form') @ApiOperation({ summary: `Get a proof form data`, description: `Get a proof form data` }) - @ApiQuery( - { name: 'id', required: true } - ) - @ApiQuery( - { name: 'orgId', required: true } - ) - @ApiResponse({ status: 201, description: 'Success', type: ApiResponseDto }) + @Roles(OrgRoles.OWNER, OrgRoles.ADMIN, OrgRoles.ISSUER, OrgRoles.VERIFIER, OrgRoles.MEMBER, OrgRoles.HOLDER) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + @ApiResponse({ status: 200, description: 'Success', type: ApiResponseDto }) @ApiUnauthorizedResponse({ status: 401, description: 'Unauthorized', type: UnauthorizedErrorDto }) @ApiForbiddenResponse({ status: 403, description: 'Forbidden', type: ForbiddenErrorDto }) - @Roles(OrgRoles.OWNER, OrgRoles.ADMIN, OrgRoles.VERIFIER) - @UseGuards(AuthGuard('jwt'), OrgRolesGuard) async getProofFormData( @Res() res: Response, @GetUser() user: IUserRequest, - @Query('id') id: string, - @Query('orgId') orgId: number + @Param('proofId') id: string, + @Param('orgId') orgId: number ): Promise { const sendProofRequest = await this.verificationService.getProofFormData(id, orgId, user); const finalResponse: IResponseType = { @@ -71,75 +65,67 @@ export class VerificationController { } /** - * Get all proof presentations + * Get proof presentation by id * @param user + * @param id * @param orgId - * @returns Get all proof presentation + * @returns Get proof presentation details */ - @Get('/proofs') - @ApiTags('verifications') + @Get('/orgs/:orgId/proofs/:proofId') @ApiOperation({ - summary: `Get all proof-presentation`, - description: `Get all proof-presentation` + summary: `Get all proof presentation by proof Id`, + description: `Get all proof presentation by proof Id` }) - @ApiResponse({ status: 201, description: 'Success', type: ApiResponseDto }) + @ApiResponse({ status: 200, 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) + @Roles(OrgRoles.OWNER, OrgRoles.ADMIN, OrgRoles.ISSUER, OrgRoles.VERIFIER, OrgRoles.MEMBER, OrgRoles.HOLDER) @UseGuards(AuthGuard('jwt'), OrgRolesGuard) - async getProofPresentations( + async getProofPresentationById( @Res() res: Response, @GetUser() user: IUserRequest, - @Query('orgId') orgId: number, - @Query('threadId') threadId: string + @Param('proofId') id: string, + @Param('orgId') orgId: number ): Promise { - const proofPresentationDetails = await this.verificationService.getProofPresentations(orgId, threadId, user); + const getProofPresentationById = await this.verificationService.getProofPresentationById(id, orgId, user); const finalResponse: IResponseType = { statusCode: HttpStatus.OK, message: ResponseMessages.verification.success.fetch, - data: proofPresentationDetails.response + data: getProofPresentationById.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') + * Get all proof presentations + * @param user + * @param orgId + * @returns Get all proof presentation + */ + @Get('/orgs/:orgId/proofs') @ApiOperation({ - summary: `Get proof-presentation by Id`, - description: `Get proof-presentation by Id` + summary: `Get all proof presentations`, + description: `Get all proof presentations` }) - @ApiResponse({ status: 201, description: 'Success', type: ApiResponseDto }) + @ApiResponse({ status: 200, description: 'Success', type: ApiResponseDto }) @ApiUnauthorizedResponse({ status: 401, description: 'Unauthorized', type: UnauthorizedErrorDto }) @ApiForbiddenResponse({ status: 403, description: 'Forbidden', type: ForbiddenErrorDto }) @ApiQuery( - { name: 'orgId', required: true } + { name: 'threadId', required: false } ) - @Roles(OrgRoles.OWNER, OrgRoles.ADMIN, OrgRoles.VERIFIER) + @Roles(OrgRoles.OWNER, OrgRoles.ADMIN, OrgRoles.ISSUER, OrgRoles.VERIFIER, OrgRoles.MEMBER, OrgRoles.HOLDER) @UseGuards(AuthGuard('jwt'), OrgRolesGuard) - async getProofPresentationById( + async getProofPresentations( @Res() res: Response, @GetUser() user: IUserRequest, - @Param('id') id: string, - @Query('orgId') orgId: number + @Param('orgId') orgId: number, + @Query('threadId') threadId: string ): Promise { - const getProofPresentationById = await this.verificationService.getProofPresentationById(id, orgId, user); + const proofPresentationDetails = await this.verificationService.getProofPresentations(orgId, threadId, user); const finalResponse: IResponseType = { statusCode: HttpStatus.OK, message: ResponseMessages.verification.success.fetch, - data: getProofPresentationById.response + data: proofPresentationDetails.response }; return res.status(HttpStatus.OK).json(finalResponse); } @@ -150,8 +136,7 @@ export class VerificationController { * @param requestProof * @returns Get requested proof presentation details */ - @Post('/proofs/request-proof') - @ApiTags('verifications') + @Post('/orgs/:orgId/proofs') @ApiOperation({ summary: `Sends a proof request`, description: `Sends a proof request` @@ -160,13 +145,20 @@ export class VerificationController { @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) + @Roles(OrgRoles.OWNER, OrgRoles.ADMIN, OrgRoles.VERIFIER) async sendPresentationRequest( @Res() res: Response, @GetUser() user: IUserRequest, + @Param('orgId') orgId: number, @Body() requestProof: RequestProof ): Promise { + + for (const attrData of requestProof.attributes) { + await this.validateAttribute(attrData); + } + + requestProof.orgId = orgId; const sendProofRequest = await this.verificationService.sendProofRequest(requestProof, user); const finalResponse: IResponseType = { statusCode: HttpStatus.CREATED, @@ -183,8 +175,7 @@ export class VerificationController { * @param orgId * @returns Get verified proof presentation details */ - @Post('proofs/verify-presentation') - @ApiTags('verifications') + @Post('/orgs/:orgId/proofs/:proofId/verify') @ApiOperation({ summary: `Verify presentation`, description: `Verify presentation` @@ -192,19 +183,13 @@ export class VerificationController { @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 + @Param('proofId') id: string, + @Param('orgId') orgId: number ): Promise { const verifyPresentation = await this.verificationService.verifyPresentation(id, orgId, user); const finalResponse: IResponseType = { @@ -215,39 +200,13 @@ export class VerificationController { 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); - } - /** * Out-Of-Band Proof Presentation * @param user * @param outOfBandRequestProof * @returns Get out-of-band requested proof presentation details */ - @Post('/proofs/create-request-oob') - @ApiTags('verifications') + @Post('/orgs/:orgId/proofs/oob') @ApiOperation({ summary: `Sends a out-of-band proof request`, description: `Sends a out-of-band proof request` @@ -261,8 +220,15 @@ export class VerificationController { async sendOutOfBandPresentationRequest( @Res() res: Response, @GetUser() user: IUserRequest, - @Body() outOfBandRequestProof: OutOfBandRequestProof + @Body() outOfBandRequestProof: OutOfBandRequestProof, + @Param('orgId') orgId: number ): Promise { + + for (const attrData of outOfBandRequestProof.attributes) { + await this.validateAttribute(attrData); + } + + outOfBandRequestProof.orgId = orgId; const sendProofRequest = await this.verificationService.sendOutOfBandPresentationRequest(outOfBandRequestProof, user); const finalResponse: IResponseType = { statusCode: HttpStatus.CREATED, @@ -271,5 +237,52 @@ export class VerificationController { }; return res.status(HttpStatus.CREATED).json(finalResponse); } + + @Post('wh/:id/proofs') + @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 { + this.logger.debug(`proofPresentationPayload ::: ${JSON.stringify(proofPresentationPayload)}`); + 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); + } + + async validateAttribute( + attrData: object + ): Promise { + + if (!attrData['attributeName']) { + throw new BadRequestException('attributeName must be required'); + } else if (!attrData['schemaId']) { + throw new BadRequestException('schemaId must be required'); + } + + if (undefined !== attrData['credDefId'] && '' === attrData['credDefId'].trim()) { + throw new BadRequestException('credDefId cannot be empty'); + } + + if (undefined !== attrData['condition'] && '' === attrData['condition'].trim()) { + throw new BadRequestException('condition cannot be empty'); + } + + if (undefined !== attrData['value'] && '' === attrData['value'].trim()) { + throw new BadRequestException('value cannot be empty'); + } + } } diff --git a/apps/connection/src/connection.service.ts b/apps/connection/src/connection.service.ts index 51756a352..78b149be1 100644 --- a/apps/connection/src/connection.service.ts +++ b/apps/connection/src/connection.service.ts @@ -78,7 +78,7 @@ export class ConnectionService { return saveConnectionDetails; } catch (error) { this.logger.error(`[createLegacyConnectionInvitation] - error in connection invitation: ${error}`); - throw new RpcException(error.response); + throw new RpcException(error.response ? error.response : error); } } @@ -97,7 +97,7 @@ export class ConnectionService { return saveConnectionDetails; } catch (error) { this.logger.error(`[getConnectionWebhook] - error in fetch connection webhook: ${error}`); - throw new RpcException(error.response); + throw new RpcException(error.response ? error.response : error); } } @@ -148,8 +148,7 @@ export class ConnectionService { return urlDetails.url; } catch (error) { this.logger.error(`Error in get url in connection service: ${JSON.stringify(error)}`); - throw error; - + throw new RpcException(error.response ? error.response : error); } } @@ -209,8 +208,7 @@ export class ConnectionService { return connectionsDetails?.response; } catch (error) { this.logger.error(`Error in get url in connection service: ${JSON.stringify(error)}`); - throw error; - + throw new RpcException(error.response ? error.response : error); } } @@ -271,7 +269,7 @@ export class ConnectionService { return createConnectionInvitation?.response; } catch (error) { this.logger.error(`[getConnectionsById] - error in get connections : ${JSON.stringify(error)}`); - throw error; + throw new RpcException(error.response ? error.response : error); } } diff --git a/apps/ecosystem/dtos/accept-reject-ecosysteminvitation.dto.ts b/apps/ecosystem/dtos/accept-reject-ecosysteminvitation.dto.ts new file mode 100644 index 000000000..789408777 --- /dev/null +++ b/apps/ecosystem/dtos/accept-reject-ecosysteminvitation.dto.ts @@ -0,0 +1,9 @@ +import { Invitation } from '@credebl/enum/enum'; + +export class AcceptRejectEcosystemInvitationDto { + orgId: string; + invitationId: string; + status: Invitation; + orgName: string; + orgDid: string; +} diff --git a/apps/ecosystem/dtos/send-invitation.dto.ts b/apps/ecosystem/dtos/send-invitation.dto.ts new file mode 100644 index 000000000..476415ecd --- /dev/null +++ b/apps/ecosystem/dtos/send-invitation.dto.ts @@ -0,0 +1,12 @@ +import { ApiExtraModels } from '@nestjs/swagger'; + +@ApiExtraModels() +export class SendInvitationDto { + email: string; +} + +@ApiExtraModels() +export class BulkSendInvitationDto { + invitations: SendInvitationDto[]; + ecosystemId: string; +} \ No newline at end of file diff --git a/apps/ecosystem/dtos/update-ecosystem-invitation.dto.ts b/apps/ecosystem/dtos/update-ecosystem-invitation.dto.ts new file mode 100644 index 000000000..0badcf828 --- /dev/null +++ b/apps/ecosystem/dtos/update-ecosystem-invitation.dto.ts @@ -0,0 +1,8 @@ +export class updateEcosystemInvitationDto { + invitationId: string; + orgId: string; + status: string; + userId: string; + email: string; + roleId: string; +} \ No newline at end of file diff --git a/apps/ecosystem/dtos/update-ecosystemOrgs.dto.ts b/apps/ecosystem/dtos/update-ecosystemOrgs.dto.ts new file mode 100644 index 000000000..f87298807 --- /dev/null +++ b/apps/ecosystem/dtos/update-ecosystemOrgs.dto.ts @@ -0,0 +1,8 @@ +export class updateEcosystemOrgsDto { + orgId: string; + orgName: string; + orgDid: string; + status: string; + ecosystemId: string; + ecosystemRoleId: string; +} \ No newline at end of file diff --git a/apps/ecosystem/enums/ecosystem.enum.ts b/apps/ecosystem/enums/ecosystem.enum.ts new file mode 100644 index 000000000..ceae1e332 --- /dev/null +++ b/apps/ecosystem/enums/ecosystem.enum.ts @@ -0,0 +1,35 @@ +export enum EcosystemRoles { + ECOSYSTEM_LEAD = 'Ecosystem Lead', + ECOSYSTEM_MEMBER = 'Ecosystem Member', + ECOSYSTEM_OWNER = 'Ecosystem Owner' +} + +export enum EcosystemOrgStatus { + ACTIVE = 'ACTIVE' +} + +export enum EcosystemInvitationStatus { + ACCEPTED = 'accepted', + REJECTED = 'rejected', + PENDING = 'pending' +} + +export enum endorsementTransactionStatus { + REQUESTED = 'requested', + SIGNED = 'signed', + DECLINED = 'declined', + SUBMITED = 'submited' +} + +export enum endorsementTransactionType { + SCHEMA = 'schema', + CREDENTIAL_DEFINITION = 'credential-definition', + SIGN = 'sign', + SUBMIT = 'submit' +} + +export enum DeploymentModeType { + PROVIDER_HOSTED = 'ProviderHosted', + ON_PREMISE = 'OnPremise' +} + diff --git a/apps/ecosystem/interfaces/ecosystem.interfaces.ts b/apps/ecosystem/interfaces/ecosystem.interfaces.ts new file mode 100644 index 000000000..e77392db7 --- /dev/null +++ b/apps/ecosystem/interfaces/ecosystem.interfaces.ts @@ -0,0 +1,192 @@ +import { Prisma } from "@prisma/client"; +export interface AttributeValue { + attributeName: string; + schemaDataType: string; + displayName: string; +} + +export interface RequestSchemaEndorsement { + orgId: number + name: string; + version: string; + attributes: AttributeValue[]; + endorse?: boolean; +} + +export interface RequestCredDeffEndorsement { + schemaId: string + tag: string; + endorse?: boolean; + schemaDetails?: object; +} + +export interface IAttributeValue { + attributeName: string; + schemaDataType: string; + displayName: string +} + +export interface SchemaTransactionPayload { + endorserDid: string; + endorse: boolean; + attributes: string[]; + version: string; + name: string; + issuerId: string; +} + +export interface CredDefTransactionPayload { + endorserDid: string; + endorse: boolean; + tag: string; + schemaId: string; + issuerId: string; +} + +export interface SchemaMessage { + message?: { + jobId: string; + schemaState: { + state: string; + action: string; + schemaId: string; + schema: Record; + schemaRequest: string; + }; + registrationMetadata: Record; + schemaMetadata: Record; + }; +} + +export interface CredDefMessage { + message?: { + jobId: string; + credentialDefinitionState: { + state: string; + action: string; + schemaId: string; + schema: Record; + credentialDefinitionRequest: string; + credentialDefinition: Record; + }; + registrationMetadata: Record; + schemaMetadata: Record; + }; +} +export interface SchemaTransactionResponse { + endorserDid: string; + authorDid: string; + requestPayload: string; + status: string; + ecosystemOrgId: string; +} + +export interface SignedTransactionMessage { + message?: { + signedTransaction: string; + }; +} + +export interface EndorsementTransactionPayload { + id: string; + endorserDid: string; + authorDid: string; + requestPayload: string; + responsePayload: string; + requestBody: Prisma.JsonValue + status: string; + ecosystemOrgId: string; + createDateTime: Date; + createdBy: number; + lastChangedDateTime: Date; + lastChangedBy: number; + deletedAt?: Date; + type?: string; + ecosystemOrgs?: { + orgId: string; + }; +} + +interface SchemaPayload { + attributes: string[]; + version: string; + name: string; + issuerId: string; +} + +interface CredentialDefinitionPayload { + tag: string; + issuerId: string; + schemaId: string; + type: string; + value: Record; +} + +export interface submitTransactionPayload { + endorsedTransaction: string; + endorserDid: string; + schema?: SchemaPayload; + credentialDefinition?: CredentialDefinitionPayload; +} + + +export interface SaveSchema { + name: string; + version: string; + attributes: string; + schemaLedgerId: string; + issuerId: string; + createdBy: string; + lastChangedBy: string; + publisherDid: string; + orgId: string; + ledgerId: number; +} + +export interface saveCredDef { + schemaLedgerId: string; + tag: string; + credentialDefinitionId: string; + revocable: boolean; + createdBy: string; + orgId: number; + schemaId: number; +} + +export interface EndorsementTransactionPayloadDetails { + id: string; + endorserDid: string; + authorDid: string; + requestPayload: string; + responsePayload: string; + type: string; + createDateTime: Date; + createdBy: number; + lastChangedDateTime: Date; + lastChangedBy: number; + deletedAt: Date | null; + status: string; + ecosystemOrgId: string; + requestBody: unknown; + ecosystemOrgs?: { + orgId: string; + }; +} + +export interface CreateEcosystem { + name: string; + + description?: string; + + tags?: string; + + userId: number; + + logo?: string; + + orgName: string; + + orgDid: string; + + orgId?: string; +} \ No newline at end of file diff --git a/apps/ecosystem/interfaces/ecosystemMembers.interface.ts b/apps/ecosystem/interfaces/ecosystemMembers.interface.ts new file mode 100644 index 000000000..563f4228b --- /dev/null +++ b/apps/ecosystem/interfaces/ecosystemMembers.interface.ts @@ -0,0 +1,7 @@ +export interface EcosystemMembersPayload { + ecosystemId: string; + orgId: string, + pageNumber: number; + pageSize: number; + search: string +} \ No newline at end of file diff --git a/apps/ecosystem/interfaces/endorsements.interface.ts b/apps/ecosystem/interfaces/endorsements.interface.ts new file mode 100644 index 000000000..d4c837c06 --- /dev/null +++ b/apps/ecosystem/interfaces/endorsements.interface.ts @@ -0,0 +1,9 @@ +export interface GetEndorsementsPayload { + ecosystemId: string; + orgId: string; + status: string; + pageNumber: number; + pageSize: number; + search: string; + type: string; + } \ No newline at end of file diff --git a/apps/ecosystem/interfaces/invitations.interface.ts b/apps/ecosystem/interfaces/invitations.interface.ts new file mode 100644 index 000000000..932e490ea --- /dev/null +++ b/apps/ecosystem/interfaces/invitations.interface.ts @@ -0,0 +1,9 @@ + + +export interface FetchInvitationsPayload { + ecosystemId: string; + userId: string, + pageNumber: number; + pageSize: number; + search: string +} \ No newline at end of file diff --git a/apps/ecosystem/src/ecosystem.controller.spec.ts b/apps/ecosystem/src/ecosystem.controller.spec.ts new file mode 100644 index 000000000..653289261 --- /dev/null +++ b/apps/ecosystem/src/ecosystem.controller.spec.ts @@ -0,0 +1,22 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { EcosystemController } from './ecosystem.controller'; +import { EcosystemService } from './ecosystem.service'; + +describe('EcosystemController', () => { + let ecosystemController: EcosystemController; + + beforeEach(async () => { + const app: TestingModule = await Test.createTestingModule({ + controllers: [EcosystemController], + providers: [EcosystemService] + }).compile(); + + ecosystemController = app.get(EcosystemController); + }); + + describe('root', () => { + it('should return "Hello World!"', () => { + expect(ecosystemController.getHello()).toBe('Hello World!'); + }); + }); +}); diff --git a/apps/ecosystem/src/ecosystem.controller.ts b/apps/ecosystem/src/ecosystem.controller.ts new file mode 100644 index 000000000..e996dc550 --- /dev/null +++ b/apps/ecosystem/src/ecosystem.controller.ts @@ -0,0 +1,220 @@ +import { Controller, Logger } from '@nestjs/common'; + +import { MessagePattern } from '@nestjs/microservices'; +import { EcosystemService } from './ecosystem.service'; +import { Body } from '@nestjs/common'; +import { BulkSendInvitationDto } from '../dtos/send-invitation.dto'; +import { AcceptRejectEcosystemInvitationDto } from '../dtos/accept-reject-ecosysteminvitation.dto'; +import { FetchInvitationsPayload } from '../interfaces/invitations.interface'; +import { EcosystemMembersPayload } from '../interfaces/ecosystemMembers.interface'; +import { GetEndorsementsPayload } from '../interfaces/endorsements.interface'; +import { RequestCredDeffEndorsement, RequestSchemaEndorsement } from '../interfaces/ecosystem.interfaces'; + +@Controller() +export class EcosystemController { + constructor(private readonly ecosystemService: EcosystemService) { } + private readonly logger = new Logger('EcosystemController'); + + /** + * Description: create new ecosystem + * @param payload Registration Details + * @returns Get created ecosystem details + */ + + @MessagePattern({ cmd: 'create-ecosystem' }) + async createEcosystem(@Body() payload: { createEcosystemDto }): Promise { + return this.ecosystemService.createEcosystem(payload.createEcosystemDto); + } + + /** + * Description: edit ecosystem + * @param payload updation Details + * @returns Get updated ecosystem details + */ + @MessagePattern({ cmd: 'edit-ecosystem' }) + async editEcosystem(@Body() payload: { editEcosystemDto, ecosystemId }): Promise { + return this.ecosystemService.editEcosystem(payload.editEcosystemDto, payload.ecosystemId); + } + + /** + * Description: get all ecosystems + * @param payload Registration Details + * @returns Get all ecosystem details + */ + @MessagePattern({ cmd: 'get-all-ecosystem' }) + async getAllEcosystems( + @Body() payload: { orgId: string } + ): Promise { + return this.ecosystemService.getAllEcosystem(payload); + } + + /** + * Description: get ecosystems dashboard details + * @returns Get ecosystem dashboard details + */ + @MessagePattern({ cmd: 'get-ecosystem-dashboard-details' }) + async getEcosystemDashboardDetails( + payload: { ecosystemId: string; orgId: string }): Promise { + return this.ecosystemService.getEcosystemDashboardDetails(payload.ecosystemId); + } + + + /** + * Description: get ecosystem invitations + * @returns Get sent invitation details + */ + @MessagePattern({ cmd: 'get-ecosystem-invitations' }) + async getEcosystemInvitations( + @Body() payload: { userEmail: string, status: string; pageNumber: number; pageSize: number; search: string } + ): Promise { + return this.ecosystemService.getEcosystemInvitations( + payload.userEmail, + payload.status, + payload.pageNumber, + payload.pageSize, + payload.search + ); + } + + /** + * + * @param payload + * @returns ecosystem members list + */ + @MessagePattern({ cmd: 'fetch-ecosystem-members' }) + async getEcosystemMembers( + @Body() payload: EcosystemMembersPayload + ): Promise { + return this.ecosystemService.getEcoystemMembers( + payload + ); + } + + /** + * + * @param payload + * @returns Sent ecosystem invitations status + */ + @MessagePattern({ cmd: 'send-ecosystem-invitation' }) + async createInvitation( + payload: { bulkInvitationDto: BulkSendInvitationDto; userId: string, userEmail: string } + ): Promise { + return this.ecosystemService.createInvitation(payload.bulkInvitationDto, payload.userId, payload.userEmail); + } + + /** + * + * @param payload + * @returns Ecosystem invitation status fetch-ecosystem-users + */ + @MessagePattern({ cmd: 'accept-reject-ecosystem-invitations' }) + async acceptRejectEcosystemInvitations(payload: { + acceptRejectInvitation: AcceptRejectEcosystemInvitationDto; + }): Promise { + return this.ecosystemService.acceptRejectEcosystemInvitations(payload.acceptRejectInvitation); + } + + + @MessagePattern({ cmd: 'get-sent-invitations-ecosystemId' }) + async getInvitationsByOrgId( + @Body() payload: FetchInvitationsPayload + ): Promise { + return this.ecosystemService.getInvitationsByEcosystemId( + payload + ); + } + + @MessagePattern({ cmd: 'get-endorsement-transactions' }) + async getEndorsementTransactions( + @Body() payload: GetEndorsementsPayload + ): Promise { + return this.ecosystemService.getEndorsementTransactions( + payload + ); + } + + @MessagePattern({ cmd: 'delete-ecosystem-invitations' }) + async deleteInvitation( + @Body() payload: { invitationId: string } + ): Promise { + return this.ecosystemService.deleteEcosystemInvitations( + payload.invitationId + ); + } + @MessagePattern({ cmd: 'fetch-ecosystem-org-data' }) + async fetchEcosystemOrg( + @Body() payload: { ecosystemId: string, orgId: string } + ): Promise { + return this.ecosystemService.fetchEcosystemOrg( + payload + ); + } + + /** + * + * @param payload + * @returns Schema endorsement request + */ + @MessagePattern({ cmd: 'schema-endorsement-request' }) + async schemaEndorsementRequest(payload: { requestSchemaPayload: RequestSchemaEndorsement; orgId: number, ecosystemId: string } + ): Promise { + return this.ecosystemService.requestSchemaEndorsement(payload.requestSchemaPayload, payload.orgId, payload.ecosystemId); + } + + /** + * + * @param payload + * @returns Schema endorsement request + */ + @MessagePattern({ cmd: 'credDef-endorsement-request' }) + async credDefEndorsementRequest(payload: { requestCredDefPayload: RequestCredDeffEndorsement; orgId: number; ecosystemId: string } + ): Promise { + return this.ecosystemService.requestCredDeffEndorsement(payload.requestCredDefPayload, payload.orgId, payload.ecosystemId); + } + + /** + * + * @param payload + * @returns sign endorsement request + */ + @MessagePattern({ cmd: 'sign-endorsement-transaction' }) + async signTransaction(payload: { endorsementId: string, ecosystemId: string } + ): Promise { + return this.ecosystemService.signTransaction(payload.endorsementId, payload.ecosystemId); + } + + /** + * + * @param payload + * @returns submit endorsement request + */ + @MessagePattern({ cmd: 'sumbit-endorsement-transaction' }) + async submitTransaction(payload: { endorsementId: string, ecosystemId: string } + ): Promise { + return this.ecosystemService.submitTransaction(payload.endorsementId, payload.ecosystemId); + } + + /** + * + * @param payload + * @returns auto sign and submit endorsement request + */ + @MessagePattern({ cmd: 'auto-endorsement-transaction' }) + async autoSignAndSubmitTransaction(): Promise { + return this.ecosystemService.autoSignAndSubmitTransaction(); + } + + /** + * + * @param payload + * @returns Declien Endorsement Transaction status + */ + @MessagePattern({ cmd: 'decline-endorsement-transaction' }) + async declineEndorsementRequestByLead(payload: { + ecosystemId: string, endorsementId: string + }): Promise { + return this.ecosystemService.declineEndorsementRequestByLead(payload.ecosystemId, payload.endorsementId); + } + + +} diff --git a/apps/ecosystem/src/ecosystem.module.ts b/apps/ecosystem/src/ecosystem.module.ts new file mode 100644 index 000000000..17bec847f --- /dev/null +++ b/apps/ecosystem/src/ecosystem.module.ts @@ -0,0 +1,26 @@ +import { Logger, Module } from '@nestjs/common'; +import { EcosystemController } from './ecosystem.controller'; +import { EcosystemService } from './ecosystem.service'; +import { ClientsModule, Transport } from '@nestjs/microservices'; +import { CommonModule } from '@credebl/common'; +import { EcosystemRepository } from './ecosystem.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: [EcosystemController], + providers: [EcosystemService, PrismaService, Logger, EcosystemRepository] +}) +export class EcosystemModule { } diff --git a/apps/ecosystem/src/ecosystem.repository.ts b/apps/ecosystem/src/ecosystem.repository.ts new file mode 100644 index 000000000..96c265473 --- /dev/null +++ b/apps/ecosystem/src/ecosystem.repository.ts @@ -0,0 +1,919 @@ +import { BadRequestException, Injectable, InternalServerErrorException, Logger } from '@nestjs/common'; +import { PrismaService } from '@credebl/prisma-service'; +// eslint-disable-next-line camelcase +import { credential_definition, ecosystem, ecosystem_config, ecosystem_invitations, ecosystem_orgs, ecosystem_roles, endorsement_transaction, org_agents, platform_config, schema } from '@prisma/client'; +import { DeploymentModeType, EcosystemInvitationStatus, EcosystemOrgStatus, EcosystemRoles, endorsementTransactionStatus, endorsementTransactionType } from '../enums/ecosystem.enum'; +import { updateEcosystemOrgsDto } from '../dtos/update-ecosystemOrgs.dto'; +import { SaveSchema, SchemaTransactionResponse, saveCredDef } from '../interfaces/ecosystem.interfaces'; +import { ResponseMessages } from '@credebl/common/response-messages'; +import { NotFoundException } from '@nestjs/common'; +import { CommonConstants } from '@credebl/common/common.constant'; +// eslint-disable-next-line camelcase + +@Injectable() +export class EcosystemRepository { + + constructor( + private readonly prisma: PrismaService, + private readonly logger: Logger + ) { } + + /** + * Description: create ecosystem + * @param createEcosystemDto + * @returns ecosystem + */ + // eslint-disable-next-line camelcase + async createNewEcosystem(createEcosystemDto): Promise { + try { + const transaction = await this.prisma.$transaction(async (prisma) => { + const { name, description, userId, logo, tags, orgId, orgName, orgDid } = createEcosystemDto; + const createdEcosystem = await prisma.ecosystem.create({ + data: { + name, + description, + tags, + logoUrl: logo + } + }); + let ecosystemUser; + if (createdEcosystem) { + ecosystemUser = await prisma.ecosystem_users.create({ + data: { + userId: String(userId), + ecosystemId: createdEcosystem.id + } + }); + } + + if (ecosystemUser) { + const ecosystemRoleDetails = await this.prisma.ecosystem_roles.findFirst({ + where: { + name: EcosystemRoles.ECOSYSTEM_LEAD + } + }); + ecosystemUser = await prisma.ecosystem_orgs.create({ + data: { + orgId: String(orgId), + status: EcosystemOrgStatus.ACTIVE, + ecosystemId: createdEcosystem.id, + ecosystemRoleId: ecosystemRoleDetails.id, + orgName, + orgDid, + deploymentMode: DeploymentModeType.PROVIDER_HOSTED + } + }); + } + return createdEcosystem; + }); + + return transaction; + } catch (error) { + this.logger.error(`Error in create ecosystem transaction: ${error.message}`); + throw error; + } + } + + /** + * Description: Edit ecosystem by Id + * @param editEcosystemDto + * @returns ecosystem details + */ + // eslint-disable-next-line camelcase + async updateEcosystemById(createEcosystemDto, ecosystemId): Promise { + try { + const { name, description, tags, logo } = createEcosystemDto; + const editEcosystem = await this.prisma.ecosystem.update({ + where: { id: ecosystemId }, + data: { + name, + description, + tags, + logoUrl: logo + } + }); + return editEcosystem; + } catch (error) { + this.logger.error(`Error in edit ecosystem transaction: ${error.message}`); + throw error; + } + } + + /** + * + * + * @returns Get all ecosystem details + */ + // eslint-disable-next-line camelcase + async getAllEcosystemDetails(orgId: string): Promise { + try { + const ecosystemDetails = await this.prisma.ecosystem.findMany({ + where: { + ecosystemOrgs: { + some: { + orgId + } + } + }, + include: { + ecosystemOrgs: { + where: { + orgId + }, + include: { + ecosystemRole: true + } + } + } + }); + return ecosystemDetails; + } catch (error) { + this.logger.error(`Error in get all ecosystem transaction: ${error.message}`); + throw error; + } + } + + /** + * + * @param ecosystemId + * @returns Get specific ecosystem details + */ + async getEcosystemDetails(ecosystemId: string): Promise { + try { + return this.prisma.ecosystem.findFirst({ + where: { + id: ecosystemId + } + }); + } catch (error) { + this.logger.error(`error: ${JSON.stringify(error)}`); + throw error; + } + } + + /** + * + * @param orgId + * @returns Get specific organization details from ecosystem + */ + // eslint-disable-next-line camelcase + async checkEcosystemOrgs(orgId: string): Promise { + try { + if (!orgId) { + throw new BadRequestException(ResponseMessages.ecosystem.error.invalidOrgId); + } + return this.prisma.ecosystem_orgs.findFirst({ + where: { + orgId + } + }); + } catch (error) { + this.logger.error(`error: ${JSON.stringify(error)}`); + throw error; + } + } + + /** + * + * @returns Get ecosystem dashboard card count + */ + // eslint-disable-next-line camelcase + async getEcosystemDashboardDetails(ecosystemId: string): Promise<{ membersCount: number; endorsementsCount: number; ecosystemConfigData: ecosystem_config[] }> { + try { + const membersCount = await this.getEcosystemMembersCount(ecosystemId); + const endorsementsCount = await this.getEcosystemEndorsementsCount(ecosystemId); + const ecosystemConfigData = await this.getEcosystemConfig(); + return { + membersCount, + endorsementsCount, + ecosystemConfigData + }; + } catch (error) { + this.logger.error(`error: ${JSON.stringify(error)}`); + throw error; + } + } + + // eslint-disable-next-line camelcase + async getEcosystemConfig(): Promise { + try { + const getEcosystemConfigDetails = await this.prisma.ecosystem_config.findMany(); + return getEcosystemConfigDetails; + } catch (error) { + this.logger.error(`error: ${JSON.stringify(error)}`); + throw error; + } + } + + async getEcosystemMembersCount(ecosystemId: string): Promise { + try { + const membersCount = await this.prisma.ecosystem_orgs.count( + { + where: { + ecosystemId + } + } + ); + return membersCount; + } catch (error) { + this.logger.error(`error: ${JSON.stringify(error)}`); + throw error; + } + } + + async getEcosystemEndorsementsCount(ecosystemId: string): Promise { + try { + const endorsementsCount = await this.prisma.endorsement_transaction.count({ + where: { + ecosystemOrgs: { + ecosystemId + + } + } + }); + return endorsementsCount; + } catch (error) { + this.logger.error(`error: ${JSON.stringify(error)}`); + throw error; + } + } + + /** + * + * @param queryObject + * @returns Get all ecosystem invitations + */ + async getEcosystemInvitations( + queryObject: object + // eslint-disable-next-line camelcase + ): Promise { + try { + return this.prisma.ecosystem_invitations.findMany({ + where: { + ...queryObject + }, + include: { + ecosystem: true + } + }); + } catch (error) { + this.logger.error(`error: ${JSON.stringify(error)}`); + throw error; + } + } + + + /** + * + * @param id + * @returns Invitation details + */ + // eslint-disable-next-line camelcase + async getEcosystemInvitationById(id: string): Promise { + try { + return this.prisma.ecosystem_invitations.findUnique({ + where: { + id + }, + include: { + ecosystem: true + } + }); + } catch (error) { + this.logger.error(`error: ${JSON.stringify(error)}`); + throw error; + } + } + + /** + * + * @param queryObject + * @param data + * @returns Updated ecosystem invitation response + */ + async updateEcosystemInvitation(id: string, data: object): Promise { + try { + return this.prisma.ecosystem_invitations.update({ + where: { + id: String(id) + }, + data: { + ...data + } + }); + } catch (error) { + this.logger.error(`error: ${JSON.stringify(error)}`); + throw error; + } + } + + // eslint-disable-next-line camelcase + async getEcosystemRole(name: string): Promise { + try { + return this.prisma.ecosystem_roles.findFirst({ + where: { + name + } + }); + } catch (error) { + this.logger.error(`getEcosystemRole: ${JSON.stringify(error)}`); + throw error; + } + } + + // eslint-disable-next-line camelcase + async updateEcosystemOrgs(createEcosystemOrgsDto: updateEcosystemOrgsDto): Promise { + try { + const { orgId, status, ecosystemRoleId, ecosystemId, orgName, orgDid } = createEcosystemOrgsDto; + + return this.prisma.ecosystem_orgs.create({ + data: { + orgId: String(orgId), + ecosystemId, + status, + ecosystemRoleId, + orgName, + orgDid, + deploymentMode: DeploymentModeType.PROVIDER_HOSTED + } + }); + } catch (error) { + this.logger.error(`error: ${JSON.stringify(error)}`); + throw error; + } + } + + /** + * + * @param email + * @param ecosystemId + * @param userId + * @returns + */ + async createSendInvitation( + email: string, + ecosystemId: string, + userId: string + // eslint-disable-next-line camelcase + ): Promise { + try { + return this.prisma.ecosystem_invitations.create({ + data: { + email, + userId, + ecosystem: { connect: { id: ecosystemId } }, + status: EcosystemInvitationStatus.PENDING, + orgId: '' + } + }); + } catch (error) { + this.logger.error(`error: ${JSON.stringify(error)}`); + throw error; + } + } + + async getInvitationsByEcosystemId(ecosystemId: string, pageNumber: number, pageSize: number, search = ''): Promise { + try { + const query = { + ecosystemId, + OR: [ + { email: { contains: search, mode: 'insensitive' } }, + { status: { contains: search, mode: 'insensitive' } } + ] + }; + + return await this.getEcosystemInvitationsPagination(query, pageNumber, pageSize); + } catch (error) { + this.logger.error(`error: ${JSON.stringify(error)}`); + throw error; + } + } + + + /** + * + * @param queryOptions + * @param filterOptions + * @returns users list + */ + // eslint-disable-next-line camelcase + async findEcosystemMembers(ecosystemId: string, pageNumber: number, pageSize: number, search = ''): Promise { + try { + const query = { + ecosystemId, + OR: + [{ orgId: { contains: search, mode: 'insensitive' } }] + }; + return await this.getEcosystemMembersPagination(query, pageNumber, pageSize); + } catch (error) { + this.logger.error(`error: ${JSON.stringify(error)}`); + throw new InternalServerErrorException(error); + } + } + + async getEcosystemMembersPagination(queryObject: object, pageNumber: number, pageSize: number): Promise { + try { + const result = await this.prisma.$transaction([ + this.prisma.ecosystem_orgs.findMany({ + where: { + ...queryObject + }, + include: { + ecosystem: true, + ecosystemRole: true + }, + take: pageSize, + skip: (pageNumber - 1) * pageSize, + orderBy: { + createDateTime: 'desc' + } + }), + this.prisma.ecosystem_orgs.count({ + where: { + ...queryObject + } + }) + ]); + + // eslint-disable-next-line prefer-destructuring + const members = result[0]; + // eslint-disable-next-line prefer-destructuring + const totalCount = result[1]; + const totalPages = Math.ceil(totalCount / pageSize); + + return { totalPages, members }; + } catch (error) { + this.logger.error(`error: ${JSON.stringify(error)}`); + throw error; + } + } + + + async getEcosystemInvitationsPagination(queryObject: object, pageNumber: number, pageSize: number): Promise { + try { + const result = await this.prisma.$transaction([ + this.prisma.ecosystem_invitations.findMany({ + where: { + ...queryObject + }, + include: { + ecosystem: true + }, + take: pageSize, + skip: (pageNumber - 1) * pageSize, + orderBy: { + createDateTime: 'desc' + } + }), + this.prisma.ecosystem_invitations.count({ + where: { + ...queryObject + } + }) + ]); + + // eslint-disable-next-line prefer-destructuring + const invitations = result[0]; + // eslint-disable-next-line prefer-destructuring + const totalCount = result[1]; + const totalPages = Math.ceil(totalCount / pageSize); + + return { totalPages, invitations }; + } catch (error) { + this.logger.error(`error: ${JSON.stringify(error)}`); + throw error; + } + } + + + async fetchEcosystemOrg( + payload: object + ): Promise { + + return this.prisma.ecosystem_orgs.findFirst({ + where: { + ...payload + }, + select: { + ecosystem: true, + ecosystemRole: true, + orgName: true + } + }); + + } + + + async getEndorsementsWithPagination(queryObject: object, pageNumber: number, pageSize: number): Promise { + try { + const result = await this.prisma.$transaction([ + this.prisma.endorsement_transaction.findMany({ + where: { + ...queryObject + }, + select: { + id: true, + endorserDid: true, + authorDid: true, + status: true, + type: true, + ecosystemOrgs: true, + requestPayload: true, + responsePayload: true, + createDateTime: true, + requestBody: true + }, + take: pageSize, + skip: (pageNumber - 1) * pageSize, + orderBy: { + createDateTime: 'desc' + } + }), + this.prisma.endorsement_transaction.count({ + where: { + ...queryObject + } + }) + ]); + + // eslint-disable-next-line prefer-destructuring + const transactions = result[0]; + // eslint-disable-next-line prefer-destructuring + const totalCount = result[1]; + const totalPages = Math.ceil(totalCount / pageSize); + + return { totalPages, transactions }; + } catch (error) { + this.logger.error(`error: ${JSON.stringify(error)}`); + throw error; + } + } + + /** + * Description: Get getAgentEndPoint by orgId + * @param orgId + * @returns Get getAgentEndPoint details + */ + // eslint-disable-next-line camelcase + async getAgentDetails(orgId: number): Promise { + try { + if (!orgId) { + throw new InternalServerErrorException(ResponseMessages.ecosystem.error.invalidOrgId); + } + const agentDetails = await this.prisma.org_agents.findFirst({ + where: { + orgId + } + }); + return agentDetails; + + } catch (error) { + this.logger.error(`Error in getting getAgentEndPoint for the ecosystem: ${error.message} `); + throw error; + } + } + + /** + * Description: Get getAgentEndPoint by invalidEcosystemId + * @param invalidEcosystemId + * @returns Get getAgentEndPoint details + */ + // eslint-disable-next-line camelcase + async getEcosystemLeadDetails(ecosystemId: string): Promise { + try { + if (!ecosystemId) { + throw new InternalServerErrorException(ResponseMessages.ecosystem.error.invalidEcosystemId); + } + const ecosystemRoleDetails = await this.prisma.ecosystem_roles.findFirst({ + where: { + name: EcosystemRoles.ECOSYSTEM_LEAD + } + }); + const ecosystemLeadDetails = await this.prisma.ecosystem_orgs.findFirst({ + where: { + ecosystemRoleId: ecosystemRoleDetails.id, + ecosystemId + } + }); + return ecosystemLeadDetails; + + } catch (error) { + this.logger.error(`Error in getting ecosystem lead details for the ecosystem: ${error.message} `); + throw error; + } + } + + /** + * 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(`Error in getting getPlatformConfigDetails for the ecosystem - error: ${JSON.stringify(error)}`); + throw error; + } + } + + /** + * Get platform config details + * @returns + */ + // eslint-disable-next-line camelcase + async getEcosystemConfigDetails(key: string): Promise { + try { + + return this.prisma.ecosystem_config.findFirst({ + where: { + key + } + }); + + } catch (error) { + this.logger.error(`Error in getting getPlatformConfigDetails for the ecosystem - error: ${JSON.stringify(error)}`); + throw error; + } + } + + async storeTransactionRequest( + schemaTransactionResponse: SchemaTransactionResponse, + requestBody: object, + type: endorsementTransactionType + ): Promise { + try { + const { endorserDid, authorDid, requestPayload, status, ecosystemOrgId } = schemaTransactionResponse; + return await this.prisma.endorsement_transaction.create({ + data: { + endorserDid, + authorDid, + requestPayload, + status, + ecosystemOrgId, + responsePayload: '', + type, + requestBody + } + }); + } catch (error) { + this.logger.error(`error: ${JSON.stringify(error)}`); + throw error; + } + } + + // eslint-disable-next-line camelcase + async deleteInvitations(invitationId: string): Promise { + try { + const deletedInvitation = await this.prisma.ecosystem_invitations.delete({ + where: { + id: invitationId, + status: EcosystemInvitationStatus.PENDING + } + }); + return deletedInvitation; + } catch (error) { + this.logger.error(`error: ${JSON.stringify(error)}`); + throw error; + } + } + + + // eslint-disable-next-line camelcase + async getEcosystemOrgDetailsbyId(orgId: string): Promise { + try { + //need to change + const ecosystemLeadDetails = await this.prisma.ecosystem_orgs.findFirst({ + where: { + orgId + } + }); + return ecosystemLeadDetails; + + } catch (error) { + this.logger.error(`Error in getting ecosystem lead details for the ecosystem: ${error.message} `); + throw error; + } + } + // eslint-disable-next-line camelcase + async getEndorsementTransactionById(endorsementId: string, status: endorsementTransactionStatus): Promise { + try { + const ecosystemLeadDetails = await this.prisma.endorsement_transaction.findFirst({ + where: { + id: endorsementId, + status + }, + include: { + ecosystemOrgs: { + select: { + orgId: true + } + } + } + }); + + return ecosystemLeadDetails; + + } catch (error) { + this.logger.error(`Error in getting ecosystem lead details for the ecosystem: ${error.message} `); + throw error; + } + } + + // eslint-disable-next-line camelcase + async findRecordsByNameAndVersion(name: string, version: string): Promise { + try { + return this.prisma.$queryRaw`SELECT * FROM endorsement_transaction WHERE "requestBody"->>'name' = ${name} AND "requestBody"->>'version' = ${version}`; + } catch (error) { + this.logger.error(`Error in getting ecosystem schema: ${error.message} `); + throw error; + } + } + + // eslint-disable-next-line camelcase + async findRecordsByCredDefTag(tag: string): Promise { + try { + return this.prisma.$queryRaw`SELECT * FROM endorsement_transaction WHERE "requestBody"->>'tag' = ${tag}`; + } catch (error) { + this.logger.error(`Error in getting ecosystem credential-definition: ${error.message} `); + throw error; + } + } + + async updateTransactionDetails( + endorsementId: string, + schemaTransactionRequest: string + + // eslint-disable-next-line camelcase, + ): Promise { + try { + const updatedTransaction = await this.prisma.endorsement_transaction.update({ + where: { id: endorsementId }, + data: { + responsePayload: schemaTransactionRequest, + status: endorsementTransactionStatus.SIGNED + } + }); + + return updatedTransaction; + + } catch (error) { + this.logger.error(`Error in updating endorsement transaction: ${error.message}`); + throw error; + } + } + + async updateTransactionStatus( + endorsementId: string, + status: endorsementTransactionStatus + // eslint-disable-next-line camelcase, + ): Promise { + try { + const updatedTransaction = await this.prisma.endorsement_transaction.update({ + where: { id: endorsementId }, + data: { + status + } + }); + + return updatedTransaction; + + } catch (error) { + this.logger.error(`Error in updating endorsement transaction: ${error.message}`); + throw error; + } + } + + async saveSchema(schemaResult: SaveSchema): Promise { + try { + const { name, version, attributes, schemaLedgerId, issuerId, createdBy, lastChangedBy, publisherDid, orgId, ledgerId } = schemaResult; + const saveResult = await this.prisma.schema.create({ + data: { + name, + version, + attributes, + schemaLedgerId, + issuerId, + createdBy: Number(createdBy), + lastChangedBy: Number(lastChangedBy), + publisherDid, + orgId: Number(orgId), + ledgerId + } + }); + return saveResult; + } catch (error) { + this.logger.error(`Error in storing schema for submit transaction: ${error.message} `); + throw error; + } + } + + // eslint-disable-next-line camelcase + async saveCredDef(credDefResult: saveCredDef): Promise { + try { + const { schemaLedgerId, tag, credentialDefinitionId, revocable, createdBy, orgId, schemaId } = credDefResult; + const saveResult = await this.prisma.credential_definition.create({ + data: { + schemaLedgerId, + tag, + credentialDefinitionId, + revocable, + createdBy: Number(createdBy), + orgId: Number(orgId), + schemaId + } + }); + return saveResult; + } catch (error) { + this.logger.error(`Error in saving credential-definition for submit transaction: ${error.message} `); + throw error; + } + } + + async getSchemaDetailsById(schemaLedgerId: string): Promise { + try { + const schemaDetails = await this.prisma.schema.findFirst({ + where: { + schemaLedgerId + } + }); + return schemaDetails; + } catch (error) { + this.logger.error(`Error in fetching schema details for submit transaction: ${error.message}`); + throw error; + } + } + + async updateEndorsementRequestStatus(ecosystemId: string, endorsementId: string): Promise { + try { + + const endorsementTransaction = await this.prisma.endorsement_transaction.findUnique({ + where: { id: endorsementId, status: endorsementTransactionStatus.REQUESTED } + }); + + if (!endorsementTransaction) { + throw new NotFoundException(ResponseMessages.ecosystem.error.EndorsementTransactionNotFoundException); + } + const { ecosystemOrgId } = endorsementTransaction; + + const endorsementTransactionEcosystemOrg = await this.prisma.ecosystem_orgs.findUnique({ + where: { id: ecosystemOrgId } + }); + + if (endorsementTransactionEcosystemOrg.ecosystemId === ecosystemId) { + const updatedEndorsementTransaction = await this.prisma.endorsement_transaction.update({ + where: { id: endorsementId }, + data: { + status: endorsementTransactionStatus.DECLINED + } + }); + + return updatedEndorsementTransaction; + } else { + throw new NotFoundException(ResponseMessages.ecosystem.error.OrgOrEcosystemNotFoundExceptionForEndorsementTransaction); + } + } catch (error) { + this.logger.error(`Error in updating endorsement transaction status: ${error.message}`); + throw error; + } + } + + async updateAutoSignAndSubmitTransaction(): Promise<{ + id: string; + key: string; + value: string; + createDateTime: Date; + createdBy: string; + lastChangedDateTime: Date; + lastChangedBy: string; + deletedAt: Date; + }> { + try { + + const { id, value } = await this.prisma.ecosystem_config.findFirst({ + where: { + key: `${CommonConstants.ECOSYSTEM_AUTO_ENDOSEMENT}` + } + }); + + const updatedValue = 'false' === value ? 'true' : 'false'; + + const updateEcosystemConfig = await this.prisma.ecosystem_config.update({ + where: { + id + }, + data: { + value: updatedValue + } + }); + + return updateEcosystemConfig; + + } catch (error) { + this.logger.error(`Error in update auto sign and submit flag: ${error.message}`); + throw error; + } + } +} diff --git a/apps/ecosystem/src/ecosystem.service.ts b/apps/ecosystem/src/ecosystem.service.ts new file mode 100644 index 000000000..874ae6f4b --- /dev/null +++ b/apps/ecosystem/src/ecosystem.service.ts @@ -0,0 +1,1005 @@ +// eslint-disable-next-line camelcase +import { ConflictException, ForbiddenException, HttpException, Inject, Injectable, InternalServerErrorException, Logger, NotFoundException } from '@nestjs/common'; +import { EcosystemRepository } from './ecosystem.repository'; +import { ResponseMessages } from '@credebl/common/response-messages'; +import { BulkSendInvitationDto } from '../dtos/send-invitation.dto'; +import { ClientProxy, RpcException } from '@nestjs/microservices'; +import { PrismaService } from '@credebl/prisma-service'; +import { EcosystemInviteTemplate } from '../templates/EcosystemInviteTemplate'; +import { EmailDto } from '@credebl/common/dtos/email.dto'; +import { sendEmail } from '@credebl/common/send-grid-helper-file'; +import { AcceptRejectEcosystemInvitationDto } from '../dtos/accept-reject-ecosysteminvitation.dto'; +import { Invitation, OrgAgentType } from '@credebl/enum/enum'; +import { EcosystemOrgStatus, EcosystemRoles, endorsementTransactionStatus, endorsementTransactionType } from '../enums/ecosystem.enum'; +import { FetchInvitationsPayload } from '../interfaces/invitations.interface'; +import { EcosystemMembersPayload } from '../interfaces/ecosystemMembers.interface'; +import { CreateEcosystem, CredDefMessage, RequestCredDeffEndorsement, RequestSchemaEndorsement, SaveSchema, SchemaMessage, SignedTransactionMessage, saveCredDef, submitTransactionPayload } from '../interfaces/ecosystem.interfaces'; +import { GetEndorsementsPayload } from '../interfaces/endorsements.interface'; +import { CommonConstants } from '@credebl/common/common.constant'; +// eslint-disable-next-line camelcase +import { credential_definition, org_agents, platform_config, schema, user } from '@prisma/client'; + + +@Injectable() +export class EcosystemService { + constructor( + @Inject('NATS_CLIENT') private readonly ecosystemServiceProxy: ClientProxy, + private readonly ecosystemRepository: EcosystemRepository, + private readonly logger: Logger, + private readonly prisma: PrismaService + + ) { } + + /** + * + * @param createEcosystemDto + * @returns + */ + + // eslint-disable-next-line camelcase + async createEcosystem(createEcosystemDto: CreateEcosystem): Promise { + const checkOrganization = await this.ecosystemRepository.checkEcosystemOrgs(createEcosystemDto.orgId); + if (checkOrganization) { + throw new ConflictException(ResponseMessages.ecosystem.error.ecosystemOrgAlready); + }; + const createEcosystem = await this.ecosystemRepository.createNewEcosystem(createEcosystemDto); + if (!createEcosystem) { + throw new NotFoundException(ResponseMessages.ecosystem.error.notCreated); + } + return createEcosystem; + } + + + /** + * + * @param editEcosystemDto + * @returns + */ + + // eslint-disable-next-line camelcase + async editEcosystem(editEcosystemDto, ecosystemId): Promise { + const editOrganization = await this.ecosystemRepository.updateEcosystemById(editEcosystemDto, ecosystemId); + if (!editOrganization) { + throw new NotFoundException(ResponseMessages.ecosystem.error.update); + } + return editOrganization; + } + + /** + * + * + * @returns all ecosystem details + */ + + // eslint-disable-next-line camelcase + async getAllEcosystem(payload: { orgId: string }): Promise { + const getAllEcosystemDetails = await this.ecosystemRepository.getAllEcosystemDetails(payload.orgId); + + if (!getAllEcosystemDetails) { + throw new NotFoundException(ResponseMessages.ecosystem.error.update); + } + return getAllEcosystemDetails; + } + + /** + * + * + * @returns ecosystem dashboard details + */ + async getEcosystemDashboardDetails(ecosystemId: string): Promise { + try { + const endorseMemberCount = await this.ecosystemRepository.getEcosystemDashboardDetails(ecosystemId); + + const query = { + ecosystemId, + ecosystemRole: { + name: EcosystemRoles.ECOSYSTEM_LEAD + } + }; + + const ecosystemDetails = await this.ecosystemRepository.fetchEcosystemOrg( + query + ); + + const dashboardDetails = { + ecosystem: ecosystemDetails['ecosystem'], + membersCount: endorseMemberCount.membersCount, + endorsementsCount: endorseMemberCount.endorsementsCount, + ecosystemLead: { + role: ecosystemDetails['ecosystemRole']['name'], + orgName: ecosystemDetails['orgName'], + config: endorseMemberCount.ecosystemConfigData + } + }; + + return dashboardDetails; + } catch (error) { + this.logger.error(`In ecosystem dashboard details : ${JSON.stringify(error)}`); + throw new RpcException(error.response ? error.response : error); + } + } + + /** + * Description: get an ecosystem invitation + * @returns Get sent ecosystem invitation details + */ + + // eslint-disable-next-line camelcase + async getEcosystemInvitations(userEmail: string, status: string, pageNumber: number, pageSize: number, search: string): Promise { + + try { + const query = { + AND: [ + { email: userEmail }, + { status: { contains: search, mode: 'insensitive' } } + ] + }; + + return await this.ecosystemRepository.getEcosystemInvitationsPagination(query, pageNumber, pageSize); + } catch (error) { + this.logger.error(`In error getEcosystemInvitations: ${JSON.stringify(error)}`); + throw new InternalServerErrorException(error); + } + } + + + /** + * + * @param bulkInvitationDto + * @param userId + * @returns + */ + async createInvitation(bulkInvitationDto: BulkSendInvitationDto, userId: string, userEmail: string): Promise { + const { invitations, ecosystemId } = bulkInvitationDto; + + try { + const ecosystemDetails = await this.ecosystemRepository.getEcosystemDetails(ecosystemId); + + for (const invitation of invitations) { + const { email } = invitation; + + const isUserExist = await this.checkUserExistInPlatform(email); + + const isInvitationExist = await this.checkInvitationExist(email, ecosystemId); + + if (!isInvitationExist && userEmail === invitation.email) { + await this.ecosystemRepository.createSendInvitation(email, ecosystemId, userId); + try { + await this.sendInviteEmailTemplate(email, ecosystemDetails.name, isUserExist); + } catch (error) { + throw new InternalServerErrorException(ResponseMessages.user.error.emailSend); + } + } + } + return ResponseMessages.ecosystem.success.createInvitation; + } catch (error) { + this.logger.error(`In send Invitation : ${JSON.stringify(error)}`); + throw new RpcException(error.response ? error.response : error); + } + } + + /** + * + * @param acceptRejectEcosystemInvitation + * @param userId + * @returns Ecosystem invitation status + */ + async acceptRejectEcosystemInvitations(acceptRejectInvitation: AcceptRejectEcosystemInvitationDto): Promise { + try { + const checkOrganization = await this.ecosystemRepository.checkEcosystemOrgs(acceptRejectInvitation.orgId); + + if (checkOrganization) { + throw new ConflictException(ResponseMessages.ecosystem.error.ecosystemOrgAlready); + }; + const { orgId, status, invitationId, orgName, orgDid } = acceptRejectInvitation; + const invitation = await this.ecosystemRepository.getEcosystemInvitationById(invitationId); + + if (!invitation) { + throw new NotFoundException(ResponseMessages.ecosystem.error.invitationNotFound); + } + + const updatedInvitation = await this.updateEcosystemInvitation(invitationId, orgId, status); + if (!updatedInvitation) { + throw new NotFoundException(ResponseMessages.ecosystem.error.invitationNotUpdate); + } + + if (status === Invitation.REJECTED) { + return ResponseMessages.ecosystem.success.invitationReject; + } + + const ecosystemRole = await this.ecosystemRepository.getEcosystemRole(EcosystemRoles.ECOSYSTEM_MEMBER); + const updateEcosystemOrgs = await this.updatedEcosystemOrgs(orgId, orgName, orgDid, invitation.ecosystemId, ecosystemRole.id); + + if (!updateEcosystemOrgs) { + throw new NotFoundException(ResponseMessages.ecosystem.error.orgsNotUpdate); + } + return ResponseMessages.ecosystem.success.invitationAccept; + + } catch (error) { + this.logger.error(`acceptRejectInvitations: ${error}`); + throw new RpcException(error.response ? error.response : error); + } + } + + async updatedEcosystemOrgs(orgId: string, orgName: string, orgDid: string, ecosystemId: string, ecosystemRoleId: string): Promise { + try { + const data = { + orgId, + status: EcosystemOrgStatus.ACTIVE, + ecosystemId, + ecosystemRoleId, + orgName, + orgDid + }; + return await this.ecosystemRepository.updateEcosystemOrgs(data); + } catch (error) { + this.logger.error(`In newEcosystemMneber : ${error}`); + throw new RpcException(error.response ? error.response : error); + } + } + + /** + * + * @param payload + * @returns Updated invitation response + */ + async updateEcosystemInvitation(invitationId: string, orgId: string, status: string): Promise { + try { + + const data = { + status, + orgId: String(orgId) + }; + return this.ecosystemRepository.updateEcosystemInvitation(invitationId, data); + + } catch (error) { + this.logger.error(`In updateOrgInvitation : ${error}`); + throw new RpcException(error.response ? error.response : error); + } + } + + /** + * + * @param email + * @param ecosystemId + * @returns Returns boolean status for invitation + */ + async checkInvitationExist( + email: string, + ecosystemId: string + ): Promise { + try { + + const query = { + email, + ecosystemId + }; + + const invitations = await this.ecosystemRepository.getEcosystemInvitations(query); + + let isPendingInvitation = false; + let isAcceptedInvitation = false; + + for (const invitation of invitations) { + if (invitation.status === Invitation.PENDING) { + isPendingInvitation = true; + } + if (invitation.status === Invitation.ACCEPTED) { + isAcceptedInvitation = true; + } + } + + if (isPendingInvitation || isAcceptedInvitation) { + return true; + } + + return false; + } catch (error) { + throw new RpcException(error.response ? error.response : error); + } + } + + /** + * + * @param email + * @param ecosystemName + * @returns Send invitation mail + */ + async sendInviteEmailTemplate( + email: string, + ecosystemName: string, + isUserExist: boolean + ): Promise { + const platformConfigData = await this.prisma.platform_config.findMany(); + + const urlEmailTemplate = new EcosystemInviteTemplate(); + const emailData = new EmailDto(); + emailData.emailFrom = platformConfigData[0].emailFrom; + emailData.emailTo = email; + emailData.emailSubject = `${process.env.PLATFORM_NAME} Platform: Invitation`; + + emailData.emailHtml = await urlEmailTemplate.sendInviteEmailTemplate(email, ecosystemName, 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.ecosystemServiceProxy + .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; + } + + /** + * + * @param RequestSchemaEndorsement + * @returns + */ + async requestSchemaEndorsement(requestSchemaPayload: RequestSchemaEndorsement, orgId: number, ecosystemId: string): Promise { + try { + const getEcosystemLeadDetails = await this.ecosystemRepository.getEcosystemLeadDetails(ecosystemId); + + const [schemaRequestExist, ecosystemMemberDetails, platformConfig, ecosystemLeadAgentDetails, getEcosystemOrgDetailsByOrgId] = await Promise.all([ + this.ecosystemRepository.findRecordsByNameAndVersion(requestSchemaPayload?.name, requestSchemaPayload?.version), + this.ecosystemRepository.getAgentDetails(orgId), + this.ecosystemRepository.getPlatformConfigDetails(), + this.ecosystemRepository.getAgentDetails(Number(getEcosystemLeadDetails.orgId)), + this.ecosystemRepository.getEcosystemOrgDetailsbyId(String(orgId)) + ]); + + if (0 !== schemaRequestExist.length) { + throw new ConflictException(ResponseMessages.ecosystem.error.schemaAlreadyExist); + } + + if (!ecosystemMemberDetails) { + throw new NotFoundException(ResponseMessages.ecosystem.error.notFound); + } + + if (!platformConfig) { + throw new NotFoundException(ResponseMessages.ecosystem.error.platformConfigNotFound); + } + + if (!getEcosystemLeadDetails) { + throw new NotFoundException(ResponseMessages.ecosystem.error.ecosystemNotFound); + } + + if (!ecosystemLeadAgentDetails) { + throw new NotFoundException(ResponseMessages.ecosystem.error.leadNotFound); + } + + if (!getEcosystemOrgDetailsByOrgId) { + throw new NotFoundException(ResponseMessages.ecosystem.error.ecosystemOrgNotFound); + } + + const url = await this.getAgentUrl(ecosystemMemberDetails.orgAgentTypeId, ecosystemMemberDetails.agentEndPoint, endorsementTransactionType.SCHEMA, ecosystemMemberDetails.tenantId); + + const attributeArray = requestSchemaPayload.attributes.map(item => item.attributeName); + + const schemaTransactionPayload = { + endorserDid: ecosystemLeadAgentDetails.orgDid, + endorse: requestSchemaPayload.endorse, + attributes: attributeArray, + version: String(requestSchemaPayload.version), + name: requestSchemaPayload.name, + issuerId: ecosystemMemberDetails.orgDid + }; + + const schemaTransactionRequest: SchemaMessage = await this._requestSchemaEndorsement(schemaTransactionPayload, url, platformConfig?.sgApiKey); + + const schemaTransactionResponse = { + endorserDid: ecosystemLeadAgentDetails.orgDid, + authorDid: ecosystemMemberDetails.orgDid, + requestPayload: schemaTransactionRequest.message.schemaState.schemaRequest, + status: endorsementTransactionStatus.REQUESTED, + ecosystemOrgId: getEcosystemOrgDetailsByOrgId.id + }; + + if ('failed' === schemaTransactionRequest.message.schemaState.state) { + throw new InternalServerErrorException(ResponseMessages.ecosystem.error.requestSchemaTransaction); + } + + return this.ecosystemRepository.storeTransactionRequest(schemaTransactionResponse, requestSchemaPayload, endorsementTransactionType.SCHEMA); + } catch (error) { + this.logger.error(`In request schema endorsement : ${JSON.stringify(error)}`); + throw new RpcException(error.response || error); + } + } + + async requestCredDeffEndorsement(requestCredDefPayload: RequestCredDeffEndorsement, orgId: number, ecosystemId: string): Promise { + try { + + const getEcosystemLeadDetails = await this.ecosystemRepository.getEcosystemLeadDetails(ecosystemId); + + const [credDefRequestExist, ecosystemMemberDetails, platformConfig, ecosystemLeadAgentDetails, getEcosystemOrgDetailsByOrgId] = await Promise.all([ + this.ecosystemRepository.findRecordsByCredDefTag(requestCredDefPayload?.tag), + this.ecosystemRepository.getAgentDetails(orgId), + this.ecosystemRepository.getPlatformConfigDetails(), + this.ecosystemRepository.getAgentDetails(Number(getEcosystemLeadDetails.orgId)), + this.ecosystemRepository.getEcosystemOrgDetailsbyId(String(orgId)) + ]); + + if (0 !== credDefRequestExist.length) { + throw new ConflictException(ResponseMessages.ecosystem.error.credDefAlreadyExist); + } + + if (!ecosystemMemberDetails) { + throw new InternalServerErrorException(ResponseMessages.ecosystem.error.notFound); + } + + if (!platformConfig) { + throw new NotFoundException(ResponseMessages.ecosystem.error.platformConfigNotFound); + } + + if (!getEcosystemLeadDetails) { + throw new NotFoundException(ResponseMessages.ecosystem.error.ecosystemNotFound); + } + + if (!ecosystemLeadAgentDetails) { + throw new InternalServerErrorException(ResponseMessages.ecosystem.error.leadNotFound); + } + + if (!getEcosystemOrgDetailsByOrgId) { + throw new NotFoundException(ResponseMessages.ecosystem.error.ecosystemOrgNotFound); + } + + const url = await this.getAgentUrl(ecosystemMemberDetails.orgAgentTypeId, ecosystemMemberDetails.agentEndPoint, endorsementTransactionType.CREDENTIAL_DEFINITION, ecosystemMemberDetails.tenantId); + + const credDefTransactionPayload = { + endorserDid: ecosystemLeadAgentDetails.orgDid, + endorse: requestCredDefPayload.endorse, + tag: requestCredDefPayload.tag, + schemaId: requestCredDefPayload.schemaId, + issuerId: ecosystemMemberDetails.orgDid + }; + + const credDefTransactionRequest: CredDefMessage = await this._requestCredDeffEndorsement(credDefTransactionPayload, url, platformConfig?.sgApiKey); + + if ('failed' === credDefTransactionRequest.message.credentialDefinitionState.state) { + throw new InternalServerErrorException(ResponseMessages.ecosystem.error.requestCredDefTransaction); + } + + const requestBody = credDefTransactionRequest.message.credentialDefinitionState.credentialDefinition; + + if (!requestBody) { + throw new NotFoundException(ResponseMessages.ecosystem.error.credentialDefinitionNotFound); + } + + requestCredDefPayload["credentialDefinition"] = requestBody; + const schemaTransactionResponse = { + endorserDid: ecosystemLeadAgentDetails.orgDid, + authorDid: ecosystemMemberDetails.orgDid, + requestPayload: credDefTransactionRequest.message.credentialDefinitionState.credentialDefinitionRequest, + status: endorsementTransactionStatus.REQUESTED, + ecosystemOrgId: getEcosystemOrgDetailsByOrgId.id + }; + + return this.ecosystemRepository.storeTransactionRequest(schemaTransactionResponse, requestCredDefPayload, endorsementTransactionType.CREDENTIAL_DEFINITION); + } catch (error) { + this.logger.error(`In request cred-def endorsement: ${JSON.stringify(error)}`); + throw new RpcException(error.response || error); + } + } + + + async getInvitationsByEcosystemId( + payload: FetchInvitationsPayload + ): Promise { + try { + + const { ecosystemId, pageNumber, pageSize, search } = payload; + const ecosystemInvitations = await this.ecosystemRepository.getInvitationsByEcosystemId(ecosystemId, pageNumber, pageSize, search); + return ecosystemInvitations; + } catch (error) { + this.logger.error(`In getInvitationsByEcosystemId : ${JSON.stringify(error)}`); + throw new RpcException(error.response ? error.response : error); + } + } + + async _requestSchemaEndorsement(requestSchemaPayload: object, url: string, apiKey: string): Promise { + const pattern = { cmd: 'agent-schema-endorsement-request' }; + const payload = { requestSchemaPayload, url, apiKey }; + + try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const message = await this.ecosystemServiceProxy.send(pattern, payload).toPromise(); + return { message }; + } catch (error) { + this.logger.error(`catch: ${JSON.stringify(error)}`); + throw new HttpException({ + status: error.status, + error: error.message + }, error.status); + } + } + + async _requestCredDeffEndorsement(requestSchemaPayload: object, url: string, apiKey: string): Promise { + const pattern = { cmd: 'agent-credDef-endorsement-request' }; + const payload = { requestSchemaPayload, url, apiKey }; + + try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const message = await this.ecosystemServiceProxy.send(pattern, payload).toPromise(); + return { message }; + } catch (error) { + this.logger.error(`catch: ${JSON.stringify(error)}`); + throw new HttpException({ + status: error.status, + error: error.message + }, error.status); + } + } + + async signTransaction(endorsementId: string, ecosystemId: string): Promise { + try { + const [endorsementTransactionPayload, ecosystemLeadDetails, platformConfig] = await Promise.all([ + this.ecosystemRepository.getEndorsementTransactionById(endorsementId, endorsementTransactionStatus.REQUESTED), + this.ecosystemRepository.getEcosystemLeadDetails(ecosystemId), + this.ecosystemRepository.getPlatformConfigDetails() + ]); + + if (!endorsementTransactionPayload) { + throw new InternalServerErrorException(ResponseMessages.ecosystem.error.invalidTransaction); + } + + if (!ecosystemLeadDetails) { + throw new InternalServerErrorException(ResponseMessages.ecosystem.error.leadNotFound); + } + + if (!platformConfig) { + throw new NotFoundException(ResponseMessages.ecosystem.error.platformConfigNotFound); + } + + const ecosystemLeadAgentDetails = await this.ecosystemRepository.getAgentDetails(Number(ecosystemLeadDetails.orgId)); + + if (!ecosystemLeadAgentDetails) { + throw new InternalServerErrorException(ResponseMessages.ecosystem.error.leadNotFound); + } + + const url = await this.getAgentUrl(ecosystemLeadAgentDetails?.orgAgentTypeId, ecosystemLeadAgentDetails.agentEndPoint, endorsementTransactionType.SIGN, ecosystemLeadAgentDetails?.tenantId); + + const jsonString = endorsementTransactionPayload.requestPayload.toString(); + const payload = { + transaction: jsonString, + endorserDid: endorsementTransactionPayload.endorserDid + }; + + const schemaTransactionRequest: SignedTransactionMessage = await this._signTransaction(payload, url, platformConfig.sgApiKey); + + if (!schemaTransactionRequest) { + throw new InternalServerErrorException(ResponseMessages.ecosystem.error.signRequestError); + } + + const autoEndorsement = `${CommonConstants.ECOSYSTEM_AUTO_ENDOSEMENT}`; + const ecosystemConfigDetails = await this.ecosystemRepository.getEcosystemConfigDetails(autoEndorsement); + + if (!ecosystemConfigDetails) { + throw new NotFoundException(ResponseMessages.ecosystem.error.ecosystemConfigNotFound); + } + + const updateSignedTransaction = await this.ecosystemRepository.updateTransactionDetails(endorsementId, schemaTransactionRequest.message.signedTransaction); + + if (!updateSignedTransaction) { + throw new InternalServerErrorException(ResponseMessages.ecosystem.error.updateTransactionError); + } + + if (updateSignedTransaction && 'true' === ecosystemConfigDetails.value) { + + const submitTxn = await this.submitTransaction(endorsementId, ecosystemId, ecosystemLeadAgentDetails.agentEndPoint); + if (!submitTxn) { + await this.ecosystemRepository.updateTransactionStatus(endorsementId, endorsementTransactionStatus.REQUESTED); + throw new InternalServerErrorException(ResponseMessages.ecosystem.error.sumbitTransaction); + } + return submitTxn; + } + + return updateSignedTransaction; + } catch (error) { + this.logger.error(`In sign transaction: ${JSON.stringify(error)}`); + throw new RpcException(error.response || error); + } + } + + /** + * + * @returns Ecosystem members list + */ + async getEcoystemMembers( + payload: EcosystemMembersPayload + ): Promise { + try { + const { ecosystemId, pageNumber, pageSize, search } = payload; + return await this.ecosystemRepository.findEcosystemMembers(ecosystemId, pageNumber, pageSize, search); + } catch (error) { + this.logger.error(`In getEcosystemMembers: ${JSON.stringify(error)}`); + throw new RpcException(error.response ? error.response : error); + } + } + + async deleteEcosystemInvitations(invitationId: string): Promise { + try { + return await this.ecosystemRepository.deleteInvitations(invitationId); + + } catch (error) { + this.logger.error(`In error deleteEcosystemInvitation: ${JSON.stringify(error)}`); + throw new RpcException(error.response ? error.response : error); + } + } + /** + * Description: Store shortening URL + * @param signEndorsementPayload + * @param url + * @returns sign message + */ + async _signTransaction(signEndorsementPayload: object, url: string, apiKey: string): Promise { + const pattern = { cmd: 'agent-sign-transaction' }; + const payload = { signEndorsementPayload, url, apiKey }; + + try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const message = await this.ecosystemServiceProxy.send(pattern, payload).toPromise(); + return { message }; + } catch (error) { + this.logger.error(`catch: ${JSON.stringify(error)}`); + throw new HttpException({ + status: error.status, + error: error.message + }, error.status); + } + } + + // eslint-disable-next-line camelcase + async getEcosystemMemberDetails(endorsementTransactionPayload): Promise { + const orgId = Number(endorsementTransactionPayload.ecosystemOrgs.orgId); + return this.ecosystemRepository.getAgentDetails(orgId); + } + + // eslint-disable-next-line camelcase + async getEcosystemLeadAgentDetails(ecosystemId): Promise { + const getEcosystemLeadDetails = await this.ecosystemRepository.getEcosystemLeadDetails(ecosystemId); + return this.ecosystemRepository.getAgentDetails(Number(getEcosystemLeadDetails.orgId)); + } + + // eslint-disable-next-line camelcase + async getPlatformConfig(): Promise { + return this.ecosystemRepository.getPlatformConfigDetails(); + } + + async submitTransactionPayload(endorsementTransactionPayload, ecosystemMemberDetails, ecosystemLeadAgentDetails): Promise { + const parsedRequestPayload = JSON.parse(endorsementTransactionPayload.responsePayload); + const jsonString = endorsementTransactionPayload.responsePayload.toString(); + const payload: submitTransactionPayload = { + endorsedTransaction: jsonString, + endorserDid: ecosystemLeadAgentDetails.orgDid + }; + + if (endorsementTransactionPayload.type === endorsementTransactionType.SCHEMA) { + payload.schema = { + attributes: parsedRequestPayload.operation.data.attr_names, + version: parsedRequestPayload.operation.data.version, + name: parsedRequestPayload.operation.data.name, + issuerId: ecosystemMemberDetails.orgDid + }; + } else if (endorsementTransactionPayload.type === endorsementTransactionType.CREDENTIAL_DEFINITION) { + + payload.credentialDefinition = { + tag: parsedRequestPayload.operation.tag, + issuerId: ecosystemMemberDetails.orgDid, + schemaId: endorsementTransactionPayload.requestBody.credentialDefinition['schemaId'], + type: endorsementTransactionPayload.requestBody.credentialDefinition['type'], + value: endorsementTransactionPayload.requestBody.credentialDefinition['value'] + }; + } + + return payload; + } + + async handleSchemaSubmission(endorsementTransactionPayload, ecosystemMemberDetails, submitTransactionRequest): Promise { + const regex = /[^:]+$/; + const match = ecosystemMemberDetails.orgDid.match(regex); + let extractedDidValue; + + if (match) { + // eslint-disable-next-line prefer-destructuring + extractedDidValue = match[0]; + + } + const saveSchemaPayload: SaveSchema = { + name: endorsementTransactionPayload.requestBody['name'], + version: endorsementTransactionPayload.requestBody['version'], + attributes: JSON.stringify(endorsementTransactionPayload.requestBody['attributes']), + schemaLedgerId: submitTransactionRequest['message'].schemaId, + issuerId: ecosystemMemberDetails.orgDid, + createdBy: endorsementTransactionPayload.ecosystemOrgs.orgId, + lastChangedBy: endorsementTransactionPayload.ecosystemOrgs.orgId, + publisherDid: extractedDidValue, + orgId: endorsementTransactionPayload.ecosystemOrgs.orgId, + ledgerId: ecosystemMemberDetails.ledgerId + }; + const saveSchemaDetails = await this.ecosystemRepository.saveSchema(saveSchemaPayload); + if (!saveSchemaDetails) { + throw new InternalServerErrorException(ResponseMessages.ecosystem.error.saveSchema); + } + return saveSchemaDetails; + } + + // eslint-disable-next-line camelcase + async handleCredDefSubmission(endorsementTransactionPayload, ecosystemMemberDetails, submitTransactionRequest): Promise { + const schemaDetails = await this.ecosystemRepository.getSchemaDetailsById(endorsementTransactionPayload.requestBody['schemaId']); + + if (!schemaDetails) { + throw new NotFoundException(ResponseMessages.ecosystem.error.schemaNotFound); + } + + const saveCredentialDefinition: saveCredDef = { + schemaLedgerId: endorsementTransactionPayload.requestBody['schemaId'], + tag: endorsementTransactionPayload.requestBody['tag'], + credentialDefinitionId: submitTransactionRequest['message'].credentialDefinitionId, + revocable: false, + createdBy: endorsementTransactionPayload.ecosystemOrgs.orgId, + orgId: ecosystemMemberDetails.orgId, + schemaId: schemaDetails.id + }; + + const saveCredDefDetails = await this.ecosystemRepository.saveCredDef(saveCredentialDefinition); + if (!saveCredDefDetails) { + throw new InternalServerErrorException(ResponseMessages.ecosystem.error.saveCredDef); + } + return saveCredDefDetails; + } + + async updateTransactionStatus(endorsementId): Promise { + return this.ecosystemRepository.updateTransactionStatus(endorsementId, endorsementTransactionStatus.SUBMITED); + } + + async submitTransaction(endorsementId, ecosystemId, ecosystemLeadAgentEndPoint?): Promise { + try { + const endorsementTransactionPayload = await this.ecosystemRepository.getEndorsementTransactionById(endorsementId, endorsementTransactionStatus.SIGNED); + if (!endorsementTransactionPayload) { + throw new InternalServerErrorException(ResponseMessages.ecosystem.error.invalidTransaction); + } + + if ('submitted' === endorsementTransactionPayload.status) { + throw new ConflictException(ResponseMessages.ecosystem.error.transactionSubmitted); + } + + const ecosystemMemberDetails = await this.getEcosystemMemberDetails(endorsementTransactionPayload); + const ecosystemLeadAgentDetails = await this.getEcosystemLeadAgentDetails(ecosystemId); + const platformConfig = await this.getPlatformConfig(); + + const agentEndPoint = ecosystemLeadAgentEndPoint ? ecosystemLeadAgentEndPoint : ecosystemMemberDetails.agentEndPoint; + const url = await this.getAgentUrl(ecosystemMemberDetails?.orgAgentTypeId, agentEndPoint, endorsementTransactionType.SUBMIT, ecosystemMemberDetails?.tenantId); + const payload = await this.submitTransactionPayload(endorsementTransactionPayload, ecosystemMemberDetails, ecosystemLeadAgentDetails); + + const submitTransactionRequest = await this._submitTransaction(payload, url, platformConfig.sgApiKey); + + if ('failed' === submitTransactionRequest["message"].state) { + throw new InternalServerErrorException(ResponseMessages.ecosystem.error.sumbitTransaction); + } + + await this.updateTransactionStatus(endorsementId); + + if (endorsementTransactionPayload.type === endorsementTransactionType.SCHEMA) { + return this.handleSchemaSubmission(endorsementTransactionPayload, ecosystemMemberDetails, submitTransactionRequest); + } else if (endorsementTransactionPayload.type === endorsementTransactionType.CREDENTIAL_DEFINITION) { + + if ('undefined' === submitTransactionRequest["message"].credentialDefinitionId.split(":")[3]) { + + const autoEndorsement = `${CommonConstants.ECOSYSTEM_AUTO_ENDOSEMENT}`; + const ecosystemConfigDetails = await this.ecosystemRepository.getEcosystemConfigDetails(autoEndorsement); + + if ('true' === ecosystemConfigDetails.value) { + + await this.ecosystemRepository.updateTransactionStatus(endorsementId, endorsementTransactionStatus.REQUESTED); + } else { + + await this.ecosystemRepository.updateTransactionStatus(endorsementId, endorsementTransactionStatus.SIGNED); + } + + throw new InternalServerErrorException(ResponseMessages.ecosystem.error.sumbitTransaction); + } + + return this.handleCredDefSubmission(endorsementTransactionPayload, ecosystemMemberDetails, submitTransactionRequest); + } + } catch (error) { + this.logger.error(`In submit transaction: ${JSON.stringify(error)}`); + throw new RpcException(error.response ? error.response : error); + } + } + + + /** + * Description: Store shortening URL + * @param signEndorsementPayload + * @param url + * @returns sign message + */ + async _submitTransaction(submitEndorsementPayload: object, url: string, apiKey: string): Promise { + const pattern = { cmd: 'agent-submit-transaction' }; + const payload = { submitEndorsementPayload, url, apiKey }; + + try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const message = await this.ecosystemServiceProxy.send(pattern, payload).toPromise(); + return { message }; + } catch (error) { + this.logger.error(`catch: ${JSON.stringify(error)}`); + throw new HttpException({ + status: error.status, + error: error.message + }, error.status); + } + } + + + async getAgentUrl( + orgAgentTypeId: number, + agentEndPoint: string, + type: string, + tenantId?: string + ): Promise { + try { + let url; + + if (orgAgentTypeId === OrgAgentType.DEDICATED) { + if (type === endorsementTransactionType.SCHEMA) { + url = `${agentEndPoint}${CommonConstants.URL_SCHM_CREATE_SCHEMA}`; + } else if (type === endorsementTransactionType.CREDENTIAL_DEFINITION) { + url = `${agentEndPoint}${CommonConstants.URL_SCHM_CREATE_CRED_DEF}`; + } else if (type === endorsementTransactionType.SIGN) { + url = `${agentEndPoint}${CommonConstants.SIGN_TRANSACTION}`; + } else if (type === endorsementTransactionType.SUBMIT) { + url = `${agentEndPoint}${CommonConstants.SUBMIT_TRANSACTION}`; + } + } else if (orgAgentTypeId === OrgAgentType.SHARED) { + // TODO + if (tenantId !== undefined) { + if (type === endorsementTransactionType.SCHEMA) { + url = `${agentEndPoint}${CommonConstants.TRANSACTION_MULTITENANT_SCHEMA}`.replace('#', tenantId); + } else if (type === endorsementTransactionType.CREDENTIAL_DEFINITION) { + url = `${agentEndPoint}${CommonConstants.TRANSACTION_MULTITENANT_CRED_DEF}`.replace('#', tenantId); + } else if (type === endorsementTransactionType.SIGN) { + url = `${agentEndPoint}${CommonConstants.TRANSACTION_MULTITENANT_SIGN}`.replace('#', tenantId); + } else if (type === endorsementTransactionType.SUBMIT) { + url = `${agentEndPoint}${CommonConstants.TRANSACTION_MULTITENANT_SUMBIT}`.replace('#', tenantId); + } else { + throw new InternalServerErrorException(ResponseMessages.ecosystem.error.invalidAgentUrl); + } + } else { + throw new InternalServerErrorException(ResponseMessages.ecosystem.error.invalidAgentUrl); + } + } 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; + } + } + + async fetchEcosystemOrg( + payload: { ecosystemId: string, orgId: string } + ): Promise { + + const isEcosystemEnabled = await this.checkEcosystemEnableFlag(); + + if (!isEcosystemEnabled) { + throw new ForbiddenException(ResponseMessages.ecosystem.error.ecosystemNotEnabled); + } + + return this.ecosystemRepository.fetchEcosystemOrg( + payload + ); + } + + /** + * + * @returns Returns ecosystem flag from settings + */ + async checkEcosystemEnableFlag( + ): Promise { + const ecosystemDetails = await this.prisma.ecosystem_config.findFirst( + { + where: { + key: 'enableEcosystem' + } + } + ); + + if ('true' === ecosystemDetails.value) { + return true; + } + + return false; + } + + async getEndorsementTransactions(payload: GetEndorsementsPayload): Promise { + const { ecosystemId, orgId, pageNumber, pageSize, search, type } = payload; + try { + + const queryEcoOrgs = { + ecosystemId, + orgId + }; + + const query = { + ecosystemOrgs: { + ecosystemId + }, + OR: [ + { status: { contains: search, mode: 'insensitive' } }, + { authorDid: { contains: search, mode: 'insensitive' } } + ] + }; + + const ecosystemOrgData = await this.ecosystemRepository.fetchEcosystemOrg(queryEcoOrgs); + + if (ecosystemOrgData['ecosystemRole']['name'] !== EcosystemRoles.ECOSYSTEM_LEAD) { + query.ecosystemOrgs['orgId'] = orgId; + } + + if (type) { + query['type'] = type; + } + + return await this.ecosystemRepository.getEndorsementsWithPagination(query, pageNumber, pageSize); + } catch (error) { + this.logger.error(`In error getEndorsementTransactions: ${JSON.stringify(error)}`); + throw new InternalServerErrorException(error); + } + } + + /** + * @returns EndorsementTransaction Status message + */ + + async autoSignAndSubmitTransaction(): Promise { + try { + + return await this.ecosystemRepository.updateAutoSignAndSubmitTransaction(); + } catch (error) { + this.logger.error(`error in decline endorsement request: ${error}`); + throw new InternalServerErrorException(error); + } + } + + /** + * + * @param ecosystemId + * @param endorsementId + * @param orgId + * @returns EndorsementTransactionRequest Status message + */ + + async declineEndorsementRequestByLead(ecosystemId: string, endorsementId: string): Promise { + try { + + return await this.ecosystemRepository.updateEndorsementRequestStatus(ecosystemId, endorsementId); + } catch (error) { + this.logger.error(`error in decline endorsement request: ${error}`); + throw new InternalServerErrorException(error); + } + } + +} diff --git a/apps/ecosystem/src/main.ts b/apps/ecosystem/src/main.ts new file mode 100644 index 000000000..6bd3f0259 --- /dev/null +++ b/apps/ecosystem/src/main.ts @@ -0,0 +1,23 @@ +import { NestFactory } from '@nestjs/core'; +import { EcosystemModule } from './ecosystem.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(EcosystemModule, { + transport: Transport.NATS, + options: { + servers: [`${process.env.NATS_URL}`] + } + }); + + app.useGlobalFilters(new HttpExceptionFilter()); + + await app.listen(); + logger.log('Ecosystem microservice is listening to NATS '); +} +bootstrap(); diff --git a/apps/ecosystem/templates/EcosystemInviteTemplate.ts b/apps/ecosystem/templates/EcosystemInviteTemplate.ts new file mode 100644 index 000000000..1486b1db4 --- /dev/null +++ b/apps/ecosystem/templates/EcosystemInviteTemplate.ts @@ -0,0 +1,71 @@ +export class EcosystemInviteTemplate { + + public sendInviteEmailTemplate( + email: string, + ecosystemName: string, + 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. + Please log in and accept the ecosystem “INVITATION” and participate in the ecosystem` + : `You have to register on the platform and then you can access the application. Accept the ecosystem “INVITATION” and participate in the ecosystem`; + + const year: number = new Date().getFullYear(); + + return ` + + + + + + + + + +
+ +
+

+ Hello ${email}, +

+

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

    +
  • Ecosystem: ${ecosystemName}
  • +
+ ${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/ecosystem/test/app.e2e-spec.ts b/apps/ecosystem/test/app.e2e-spec.ts new file mode 100644 index 000000000..1f1f79169 --- /dev/null +++ b/apps/ecosystem/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 { EcosystemModule } from './../src/ecosystem.module'; + +describe('EcosystemController (e2e)', () => { + let app: INestApplication; + + beforeEach(async () => { + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [EcosystemModule] + }).compile(); + + app = moduleFixture.createNestApplication(); + await app.init(); + }); + + it('/ (GET)', () => { + return request(app.getHttpServer()) + .get('/') + .expect(200) + .expect('Hello World!'); + }); +}); diff --git a/apps/ecosystem/test/jest-e2e.json b/apps/ecosystem/test/jest-e2e.json new file mode 100644 index 000000000..e9d912f3e --- /dev/null +++ b/apps/ecosystem/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/ecosystem/tsconfig.app.json b/apps/ecosystem/tsconfig.app.json new file mode 100644 index 000000000..ac6e9e1bb --- /dev/null +++ b/apps/ecosystem/tsconfig.app.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "declaration": false, + "outDir": "../../dist/apps/ecosystem" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] +} diff --git a/apps/issuance/src/issuance.service.ts b/apps/issuance/src/issuance.service.ts index f6caddcd5..1760c97fc 100644 --- a/apps/issuance/src/issuance.service.ts +++ b/apps/issuance/src/issuance.service.ts @@ -55,7 +55,7 @@ export class IssuanceService { return credentialCreateOfferDetails?.response; } catch (error) { this.logger.error(`[sendCredentialCreateOffer] - error in create credentials : ${JSON.stringify(error)}`); - throw error; + throw new RpcException(error.response ? error.response : error); } } @@ -90,7 +90,7 @@ export class IssuanceService { return credentialCreateOfferDetails?.response; } catch (error) { this.logger.error(`[sendCredentialCreateOffer] - error in create credentials : ${JSON.stringify(error)}`); - throw error; + throw new RpcException(error.response ? error.response : error); } } @@ -152,7 +152,7 @@ export class IssuanceService { return issueCredentialsDetails?.response; } catch (error) { this.logger.error(`[sendCredentialCreateOffer] - error in create credentials : ${JSON.stringify(error)}`); - throw new RpcException(error); + throw new RpcException(error.response ? error.response : error); } } @@ -203,7 +203,7 @@ export class IssuanceService { return createConnectionInvitation?.response; } catch (error) { this.logger.error(`[getIssueCredentialsbyCredentialRecordId] - error in get credentials : ${JSON.stringify(error)}`); - throw error; + throw new RpcException(error.response ? error.response : error); } } @@ -213,7 +213,7 @@ export class IssuanceService { return agentDetails; } catch (error) { this.logger.error(`[getIssueCredentialsbyCredentialRecordId] - error in get credentials : ${JSON.stringify(error)}`); - throw error; + throw new RpcException(error.response ? error.response : error); } } @@ -311,7 +311,6 @@ export class IssuanceService { } catch (error) { this.logger.error(`Error in get agent url: ${JSON.stringify(error)}`); throw error; - } } } diff --git a/apps/ledger/src/credential-definition/credential-definition.controller.ts b/apps/ledger/src/credential-definition/credential-definition.controller.ts index 7ef0d638c..1f9aab2f3 100644 --- a/apps/ledger/src/credential-definition/credential-definition.controller.ts +++ b/apps/ledger/src/credential-definition/credential-definition.controller.ts @@ -4,7 +4,7 @@ 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 { GetAllCredDefsPayload, GetCredDefBySchemaId } from './interfaces/create-credential-definition.interface'; import { CreateCredDefPayload, GetCredDefPayload } from './interfaces/create-credential-definition.interface'; import { credential_definition } from '@prisma/client'; @@ -45,4 +45,9 @@ export class CredentialDefinitionController { }> { return this.credDefService.getAllCredDefs(payload); } + + @MessagePattern({ cmd: 'get-all-credential-definitions-by-schema-id' }) + async getCredentialDefinitionBySchemaId(payload: GetCredDefBySchemaId): Promise { + return this.credDefService.getCredentialDefinitionBySchemaId(payload); + } } \ No newline at end of file diff --git a/apps/ledger/src/credential-definition/credential-definition.service.ts b/apps/ledger/src/credential-definition/credential-definition.service.ts index eb8831fcf..201bda760 100644 --- a/apps/ledger/src/credential-definition/credential-definition.service.ts +++ b/apps/ledger/src/credential-definition/credential-definition.service.ts @@ -9,7 +9,7 @@ import { 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 { CreateCredDefPayload, CredDefPayload, GetAllCredDefsPayload, GetCredDefBySchemaId, 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'; @@ -114,7 +114,7 @@ export class CredentialDefinitionService extends BaseService { this.logger.error( `Error in creating credential definition: ${JSON.stringify(error)}` ); - throw new RpcException(error.response); + throw new RpcException(error.response ? error.response : error); } } @@ -180,7 +180,7 @@ export class CredentialDefinitionService extends BaseService { return credDefResponse; } catch (error) { this.logger.error(`Error retrieving credential definition with id ${payload.credentialDefinitionId}`); - throw new RpcException(error.response); + throw new RpcException(error.response ? error.response : error); } } @@ -252,8 +252,18 @@ export class CredentialDefinitionService extends BaseService { } catch (error) { this.logger.error(`Error in retrieving credential definitions: ${error}`); - throw new RpcException(error.response); + throw new RpcException(error.response ? error.response : error); } } + async getCredentialDefinitionBySchemaId(payload: GetCredDefBySchemaId): Promise { + try { + const { schemaId } = payload; + const credDefListBySchemaId = await this.credentialDefinitionRepository.getCredentialDefinitionBySchemaId(schemaId); + return credDefListBySchemaId; + } catch (error) { + this.logger.error(`Error in retrieving credential definitions: ${error}`); + throw new RpcException(error.response ? error.response : error); + } + } } \ 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 index ccefdcf37..0f574fe19 100644 --- a/apps/ledger/src/credential-definition/interfaces/create-credential-definition.interface.ts +++ b/apps/ledger/src/credential-definition/interfaces/create-credential-definition.interface.ts @@ -50,4 +50,8 @@ export interface GetAllCredDefsPayload { credDefSearchCriteria: GetAllCredDefsDto, user: IUserRequestInterface, orgId: number +} + +export interface GetCredDefBySchemaId { + schemaId: 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 index 03690982e..12597749b 100644 --- a/apps/ledger/src/credential-definition/repositories/credential-definition.repository.ts +++ b/apps/ledger/src/credential-definition/repositories/credential-definition.repository.ts @@ -152,4 +152,18 @@ export class CredentialDefinitionRepository { throw error; } } + + async getCredentialDefinitionBySchemaId(schemaId: string): Promise { + try { + return this.prisma.credential_definition.findMany({ + where: { + schemaLedgerId: schemaId + } + + }); + } catch (error) { + this.logger.error(`Error in getting credential definitions: ${error}`); + throw error; + } + } } \ No newline at end of file diff --git a/apps/ledger/src/ledger.controller.ts b/apps/ledger/src/ledger.controller.ts index 10ec63cd2..03b57fc2d 100644 --- a/apps/ledger/src/ledger.controller.ts +++ b/apps/ledger/src/ledger.controller.ts @@ -1,8 +1,14 @@ import { Controller } from '@nestjs/common'; import { LedgerService } from './ledger.service'; +import { MessagePattern } from '@nestjs/microservices'; +import { ledgers } from '@prisma/client'; @Controller() export class LedgerController { - constructor(private readonly ledgerService: LedgerService) {} + constructor(private readonly ledgerService: LedgerService) { } + @MessagePattern({ cmd: 'get-all-ledgers' }) + async getAllLedgers(): Promise { + return this.ledgerService.getAllLedgers(); + } } diff --git a/apps/ledger/src/ledger.module.ts b/apps/ledger/src/ledger.module.ts index 3728649a8..7d65d6fc6 100644 --- a/apps/ledger/src/ledger.module.ts +++ b/apps/ledger/src/ledger.module.ts @@ -1,10 +1,11 @@ -import { Module } from '@nestjs/common'; +import { Logger, 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'; +import { LedgerRepository } from './repositories/ledger.repository'; @Module({ imports: [ @@ -18,8 +19,13 @@ import { ClientsModule, Transport } from '@nestjs/microservices'; } ]), SchemaModule, CredentialDefinitionModule -], + ], controllers: [LedgerController], - providers: [LedgerService, PrismaService] + providers: [ + LedgerService, + PrismaService, + LedgerRepository, + Logger + ] }) export class LedgerModule { } diff --git a/apps/ledger/src/ledger.service.ts b/apps/ledger/src/ledger.service.ts index 60f32c21a..b8254d3f4 100644 --- a/apps/ledger/src/ledger.service.ts +++ b/apps/ledger/src/ledger.service.ts @@ -1,5 +1,31 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, NotFoundException } from '@nestjs/common'; +import { BaseService } from 'libs/service/base.service'; +import { LedgerRepository } from './repositories/ledger.repository'; +import { RpcException } from '@nestjs/microservices'; +import { ledgers } from '@prisma/client'; +import { ResponseMessages } from '@credebl/common/response-messages'; @Injectable() -export class LedgerService { +export class LedgerService extends BaseService { + + constructor( + private readonly ledgerRepository: LedgerRepository + ) { + super('LedgerService'); + } + + async getAllLedgers(): Promise { + try { + const getAllLedgerDetails = await this.ledgerRepository.getAllLedgers(); + + if (!getAllLedgerDetails) { + throw new NotFoundException(ResponseMessages.ledger.error.NotFound); + } + + return getAllLedgerDetails; + } catch (error) { + this.logger.error(`Error in retrieving all ledgers: ${error}`); + throw new RpcException(error.response ? error.response : error); + } + } } diff --git a/apps/ledger/src/repositories/ledger.repository.ts b/apps/ledger/src/repositories/ledger.repository.ts new file mode 100644 index 000000000..c1f39ef33 --- /dev/null +++ b/apps/ledger/src/repositories/ledger.repository.ts @@ -0,0 +1,22 @@ +import { PrismaService } from "@credebl/prisma-service"; +import { Injectable, Logger } from "@nestjs/common"; +import { ledgers } from "@prisma/client"; + + +@Injectable() +export class LedgerRepository { + private readonly logger = new Logger('LedgerRepository'); + + constructor( + private prisma: PrismaService + ) { } + + async getAllLedgers(): Promise { + try { + return this.prisma.ledgers.findMany(); + } catch (error) { + this.logger.error(`Error in getAllLedgers: ${error}`); + throw error; + } + } +} \ No newline at end of file diff --git a/apps/ledger/src/schema/schema.service.ts b/apps/ledger/src/schema/schema.service.ts index ae76b2129..c1816f35d 100644 --- a/apps/ledger/src/schema/schema.service.ts +++ b/apps/ledger/src/schema/schema.service.ts @@ -180,7 +180,7 @@ export class SchemaService extends BaseService { this.logger.error( `[createSchema] - outer Error: ${JSON.stringify(error)}` ); - throw new RpcException(error.response); + throw new RpcException(error.response ? error.response : error); } } @@ -244,7 +244,7 @@ export class SchemaService extends BaseService { } catch (error) { this.logger.error(`Error in getting schema by id: ${error}`); - throw new RpcException(error.response); + throw new RpcException(error.response ? error.response : error); } } @@ -322,7 +322,7 @@ export class SchemaService extends BaseService { } catch (error) { this.logger.error(`Error in retrieving schemas by org id: ${error}`); - throw new RpcException(error.response); + throw new RpcException(error.response ? error.response : error); } } @@ -361,7 +361,7 @@ export class SchemaService extends BaseService { } catch (error) { this.logger.error(`Error in retrieving credential definition: ${error}`); - throw new RpcException(error.response); + throw new RpcException(error.response ? error.response : error); } } @@ -410,7 +410,7 @@ export class SchemaService extends BaseService { } catch (error) { this.logger.error(`Error in retrieving all schemas: ${error}`); - throw new RpcException(error.response); + throw new RpcException(error.response ? error.response : error); } } } diff --git a/apps/organization/dtos/create-organization.dto.ts b/apps/organization/dtos/create-organization.dto.ts index c2fd5249e..2d54b1ef7 100644 --- a/apps/organization/dtos/create-organization.dto.ts +++ b/apps/organization/dtos/create-organization.dto.ts @@ -6,6 +6,7 @@ export class CreateOrganizationDto { description?: string; logo?: string; website?: string; + orgSlug?:string; } export class CreateUserRoleOrgDto { diff --git a/apps/organization/interfaces/organization.interface.ts b/apps/organization/interfaces/organization.interface.ts index 5a1d9667f..f8cbb9e9c 100644 --- a/apps/organization/interfaces/organization.interface.ts +++ b/apps/organization/interfaces/organization.interface.ts @@ -18,4 +18,6 @@ export interface IUpdateOrganization { orgId: string; logo?: string; website?: string; + orgSlug?: string; + isPublic?:boolean } \ No newline at end of file diff --git a/apps/organization/repositories/organization.repository.ts b/apps/organization/repositories/organization.repository.ts index 1f44ebcf9..4ad0bd967 100644 --- a/apps/organization/repositories/organization.repository.ts +++ b/apps/organization/repositories/organization.repository.ts @@ -53,7 +53,10 @@ export class OrganizationRepository { name: createOrgDto.name, logoUrl: createOrgDto.logo, description: createOrgDto.description, - website: createOrgDto.website + website: createOrgDto.website, + orgSlug: createOrgDto.orgSlug, + publicProfile: true + } }); } catch (error) { @@ -78,10 +81,11 @@ export class OrganizationRepository { name: updateOrgDto.name, logoUrl: updateOrgDto.logo, description: updateOrgDto.description, - website: updateOrgDto.website + website: updateOrgDto.website, + orgSlug: updateOrgDto.orgSlug, + publicProfile: updateOrgDto.isPublic } }); - } catch (error) { this.logger.error(`error: ${JSON.stringify(error)}`); throw new InternalServerErrorException(error); @@ -243,7 +247,7 @@ export class OrganizationRepository { } } - async getOrganization(queryObject: object): Promise { + async getOrganization(queryObject: object): Promise { try { return this.prisma.organisation.findFirst({ where: { @@ -257,6 +261,12 @@ export class OrganizationRepository { org_agent_type: true, ledgers: true } + }, + userOrgRoles: { + include: { + user: true, + orgRole: true + } } } }); @@ -403,4 +413,24 @@ export class OrganizationRepository { throw new InternalServerErrorException(error); } } + + /** + * + * @param name + * @returns Organization exist details + */ + + async checkOrganizationExist(name: string, orgId: number): Promise { + try { + return this.prisma.organisation.findMany({ + where: { + id: orgId, + name + } + }); + } catch (error) { + this.logger.error(`error: ${JSON.stringify(error)}`); + throw new InternalServerErrorException(error); + } + } } diff --git a/apps/organization/src/organization.controller.ts b/apps/organization/src/organization.controller.ts index 6302aa0cb..6128c1592 100644 --- a/apps/organization/src/organization.controller.ts +++ b/apps/organization/src/organization.controller.ts @@ -31,8 +31,8 @@ export class OrganizationController { */ @MessagePattern({ cmd: 'update-organization' }) - async updateOrganization(payload: { updateOrgDto: IUpdateOrganization; userId: number }): Promise { - return this.organizationService.updateOrganization(payload.updateOrgDto, payload.userId); + async updateOrganization(payload: { updateOrgDto: IUpdateOrganization; userId: number, orgId: number }): Promise { + return this.organizationService.updateOrganization(payload.updateOrgDto, payload.userId, payload.orgId); } /** @@ -72,7 +72,7 @@ export class OrganizationController { } @MessagePattern({ cmd: 'get-organization-public-profile' }) - async getPublicProfile(payload: { id }): Promise { + async getPublicProfile(payload: { orgSlug }): Promise { return this.organizationService.getPublicProfile(payload); } @@ -110,9 +110,9 @@ export class OrganizationController { */ @MessagePattern({ cmd: 'send-invitation' }) async createInvitation( - @Body() payload: { bulkInvitationDto: BulkSendInvitationDto; userId: number } + @Body() payload: { bulkInvitationDto: BulkSendInvitationDto; userId: number, userEmail: string } ): Promise { - return this.organizationService.createInvitation(payload.bulkInvitationDto, payload.userId); + return this.organizationService.createInvitation(payload.bulkInvitationDto, payload.userId, payload.userEmail); } @MessagePattern({ cmd: 'fetch-user-invitations' }) diff --git a/apps/organization/src/organization.service.ts b/apps/organization/src/organization.service.ts index ec5ddfa96..55ec0e538 100644 --- a/apps/organization/src/organization.service.ts +++ b/apps/organization/src/organization.service.ts @@ -48,6 +48,9 @@ export class OrganizationService { throw new ConflictException(ResponseMessages.organisation.error.exists); } + const orgSlug = this.createOrgSlug(createOrgDto.name); + createOrgDto.orgSlug = orgSlug; + const organizationDetails = await this.organizationRepository.createOrganization(createOrgDto); const ownerRoleData = await this.orgRoleService.getRole(OrgRoles.OWNER); @@ -57,10 +60,24 @@ export class OrganizationService { return organizationDetails; } catch (error) { this.logger.error(`In create organization : ${JSON.stringify(error)}`); - throw new RpcException(error.response); + throw new RpcException(error.response ? error.response : error); } } + + /** + * + * @param orgName + * @returns OrgSlug + */ + createOrgSlug(orgName: string): string { + return orgName + .toLowerCase() // Convert the input to lowercase + .replace(/\s+/g, '-') // Replace spaces with hyphens + .replace(/[^a-z0-9-]/g, '') // Remove non-alphanumeric characters except hyphens + .replace(/--+/g, '-'); // Replace multiple consecutive hyphens with a single hyphen + } + /** * * @param registerOrgDto @@ -68,14 +85,27 @@ export class OrganizationService { */ // eslint-disable-next-line camelcase - async updateOrganization(updateOrgDto: IUpdateOrganization, userId: number): Promise { + async updateOrganization(updateOrgDto: IUpdateOrganization, userId: number, orgId: number): Promise { try { + + const organizationExist = await this.organizationRepository.checkOrganizationExist(updateOrgDto.name, orgId); + + if (0 === organizationExist.length) { + const organizationExist = await this.organizationRepository.checkOrganizationNameExist(updateOrgDto.name); + if (organizationExist) { + throw new ConflictException(ResponseMessages.organisation.error.exists); + } + } + + const orgSlug = await this.createOrgSlug(updateOrgDto.name); + updateOrgDto.orgSlug = orgSlug; + const organizationDetails = await this.organizationRepository.updateOrganization(updateOrgDto); await this.userActivityService.createActivity(userId, organizationDetails.id, `${organizationDetails.name} organization updated`, 'Organization details updated successfully'); return organizationDetails; } catch (error) { this.logger.error(`In update organization : ${JSON.stringify(error)}`); - throw new RpcException(error.response); + throw new RpcException(error.response ? error.response : error); } } @@ -111,7 +141,7 @@ export class OrganizationService { } catch (error) { this.logger.error(`In fetch getOrganizations : ${JSON.stringify(error)}`); - throw new RpcException(error.response); + throw new RpcException(error.response ? error.response : error); } } @@ -143,15 +173,16 @@ export class OrganizationService { } catch (error) { this.logger.error(`In fetch getPublicOrganizations : ${JSON.stringify(error)}`); - throw new RpcException(error.response); + throw new RpcException(error.response ? error.response : error); } } - async getPublicProfile(payload: { id }): Promise { + async getPublicProfile(payload: { orgSlug: string }): Promise { + const { orgSlug } = payload; try { const query = { - id: payload.id, + orgSlug, publicProfile: true }; @@ -163,7 +194,7 @@ export class OrganizationService { } catch (error) { this.logger.error(`get user: ${JSON.stringify(error)}`); - throw new RpcException(error.response); + throw new RpcException(error.response ? error.response : error); } } @@ -184,7 +215,7 @@ export class OrganizationService { return organizationDetails; } catch (error) { this.logger.error(`In create organization : ${JSON.stringify(error)}`); - throw new RpcException(error.response); + throw new RpcException(error.response ? error.response : error); } } @@ -204,7 +235,7 @@ export class OrganizationService { return getOrganization; } catch (error) { this.logger.error(`In create organization : ${JSON.stringify(error)}`); - throw new RpcException(error.response); + throw new RpcException(error.response ? error.response : error); } } @@ -220,7 +251,7 @@ export class OrganizationService { return this.orgRoleService.getOrgRoles(); } catch (error) { this.logger.error(`In getOrgRoles : ${JSON.stringify(error)}`); - throw new RpcException(error.response); + throw new RpcException(error.response ? error.response : error); } } @@ -242,13 +273,26 @@ export class OrganizationService { const invitations = await this.organizationRepository.getOrgInvitations(query); - if (0 < invitations.length) { + let isPendingInvitation = false; + let isAcceptedInvitation = false; + + for (const invitation of invitations) { + if (invitation.status === Invitation.PENDING) { + isPendingInvitation = true; + } + if (invitation.status === Invitation.ACCEPTED) { + isAcceptedInvitation = true; + } + } + + if (isPendingInvitation || isAcceptedInvitation) { return true; } + return false; } catch (error) { this.logger.error(`error: ${JSON.stringify(error)}`); - throw new InternalServerErrorException(error); + throw new RpcException(error.response ? error.response : error); } } @@ -259,7 +303,7 @@ export class OrganizationService { */ // eslint-disable-next-line camelcase - async createInvitation(bulkInvitationDto: BulkSendInvitationDto, userId: number): Promise { + async createInvitation(bulkInvitationDto: BulkSendInvitationDto, userId: number, userEmail: string): Promise { const { invitations, orgId } = bulkInvitationDto; try { @@ -270,9 +314,10 @@ export class OrganizationService { const isUserExist = await this.checkUserExistInPlatform(email); - const isInvitationExist = await this.checkInvitationExist(email, orgId); + const isInvitationExist = await this.checkInvitationExist(email, orgId); + + if (!isInvitationExist && userEmail !== invitation.email) { - if (!isInvitationExist) { await this.organizationRepository.createSendInvitation(email, orgId, userId, orgRoleId); const orgRolesDetails = await this.orgRoleService.getOrgRolesByIds(orgRoleId); @@ -288,7 +333,7 @@ export class OrganizationService { return ResponseMessages.organisation.success.createInvitation; } catch (error) { this.logger.error(`In send Invitation : ${JSON.stringify(error)}`); - throw new RpcException(error.response); + throw new RpcException(error.response ? error.response : error); } } @@ -351,7 +396,7 @@ export class OrganizationService { return this.organizationRepository.getAllOrgInvitations(email, status, pageNumber, pageSize, search); } catch (error) { this.logger.error(`In fetchUserInvitation : ${JSON.stringify(error)}`); - throw new RpcException(error.response); + throw new RpcException(error.response ? error.response : error); } } @@ -387,7 +432,7 @@ export class OrganizationService { } catch (error) { this.logger.error(`In updateOrgInvitation : ${error}`); - throw new RpcException(error.response); + throw new RpcException(error.response ? error.response : error); } } @@ -423,7 +468,7 @@ export class OrganizationService { } catch (error) { this.logger.error(`Error in updateUserRoles: ${JSON.stringify(error)}`); - throw new RpcException(error.response); + throw new RpcException(error.response ? error.response : error); } } @@ -432,8 +477,7 @@ export class OrganizationService { return this.organizationRepository.getOrgDashboard(orgId); } catch (error) { this.logger.error(`In create organization : ${JSON.stringify(error)}`); - throw new RpcException(error.response); + throw new RpcException(error.response ? error.response : error); } } - -} +} \ No newline at end of file diff --git a/apps/user/interfaces/user.interface.ts b/apps/user/interfaces/user.interface.ts index 051ba3bfb..f42a9b5a8 100644 --- a/apps/user/interfaces/user.interface.ts +++ b/apps/user/interfaces/user.interface.ts @@ -26,9 +26,11 @@ export interface InvitationsI { export interface UserEmailVerificationDto{ email:string + username?:string } export interface userInfo{ + email: string, password: string, firstName: string, lastName: string, @@ -51,4 +53,5 @@ export interface UpdateUserProfile { profileImg?: string; firstName: string, lastName: string, + isPublic: boolean, } \ No newline at end of file diff --git a/apps/user/repositories/user-device.repository.ts b/apps/user/repositories/user-device.repository.ts index 9e3c559d2..f5a78e691 100644 --- a/apps/user/repositories/user-device.repository.ts +++ b/apps/user/repositories/user-device.repository.ts @@ -38,254 +38,254 @@ export class UserDevicesRepository { } } -/** - * - * @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 - } - }); + /** + * + * @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; + return saveResponse; - } catch (error) { - this.logger.error(`In Create User Repository: ${JSON.stringify(error)}`); - throw error; + } 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' - } - }); + /** + * + * @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); + 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 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 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` + /** + * + * @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); + 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 + * @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 Device detail for specific credentialId + */ + // eslint-disable-next-line camelcase + async checkUserDeviceByCredentialId(credentialId: string): Promise { + this.logger.log(`checkUserDeviceByCredentialId: ${credentialId}`); + try { + return await 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 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 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` + /** + * + * @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); + } 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); + /** + * + * @param id + * @param credentialId + * @param deviceFriendlyName + * @returns Update device name for specific credentialId + */ + // eslint-disable-next-line camelcase + async addCredentialIdAndNameById(id: number, updateFidoUserDetails: string): Promise { + + try { + return await this.prisma.user_devices.update({ + where: { + id + }, + data: { + credentialId: JSON.parse(updateFidoUserDetails).credentialId, + deviceFriendlyName: JSON.parse(updateFidoUserDetails).updateFidoUserDetailsDto.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 index cfdfc92e7..e64f5033a 100644 --- a/apps/user/repositories/user.repository.ts +++ b/apps/user/repositories/user.repository.ts @@ -7,11 +7,11 @@ import { InternalServerErrorException } from '@nestjs/common'; import { PrismaService } from '@credebl/prisma-service'; // 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 + username?: string // Add more properties if needed for other unique identifier fields }; @@ -24,14 +24,14 @@ export class UserRepository { * @param userEmailVerificationDto * @returns user email */ - async createUser(userEmailVerificationDto: UserEmailVerificationDto): Promise { + async createUser(userEmailVerificationDto: UserEmailVerificationDto, verifyCode: string): Promise { try { - const verifyCode = uuidv4(); const saveResponse = await this.prisma.user.create({ data: { - username: userEmailVerificationDto.email, + username: userEmailVerificationDto.username, email: userEmailVerificationDto.email, - verificationCode: verifyCode.toString() + verificationCode: verifyCode.toString(), + publicProfile: true } }); @@ -99,19 +99,22 @@ export class UserRepository { * @param id * @returns User profile data */ - async getUserPublicProfile(id: number): Promise { - const queryOptions: UserQueryOptions = { - id - }; - return this.findUserForPublicProfile(queryOptions); - } + async getUserPublicProfile(username: string): Promise { + + const queryOptions: UserQueryOptions = { + username + }; + + return this.findUserForPublicProfile(queryOptions); + } /** * * @Body updateUserProfile * @returns Update user profile data */ - async updateUserProfile(updateUserProfile: UpdateUserProfile): Promise { + async updateUserProfile(updateUserProfile: UpdateUserProfile): Promise { + try { const userdetails = await this.prisma.user.update({ where: { @@ -120,7 +123,8 @@ export class UserRepository { data: { profileImg: updateUserProfile.profileImg, firstName: updateUserProfile.firstName, - lastName: updateUserProfile.lastName + lastName: updateUserProfile.lastName, + publicProfile: updateUserProfile?.isPublic } }); return userdetails; @@ -233,6 +237,9 @@ export class UserRepository { }, { email: queryOptions.email + }, + { + username: queryOptions.username } ] }, @@ -253,7 +260,8 @@ export class UserRepository { name: true, description: true, logoUrl: true, - website: true + website: true, + orgSlug: true }, where:{ publicProfile: true @@ -397,6 +405,7 @@ export class UserRepository { email: true, firstName: true, lastName: true, + profileImg: true, isEmailVerified: true, clientId: false, clientSecret: false, diff --git a/apps/user/src/fido/dtos/fido-user.dto.ts b/apps/user/src/fido/dtos/fido-user.dto.ts index fbf546b67..9336e4a9c 100644 --- a/apps/user/src/fido/dtos/fido-user.dto.ts +++ b/apps/user/src/fido/dtos/fido-user.dto.ts @@ -1,10 +1,7 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsArray, IsBoolean, IsEmail, IsNotEmpty, IsOptional, IsString, ValidateNested } from 'class-validator'; +import { IsArray, IsBoolean, IsOptional, IsString, ValidateNested } from 'class-validator'; export class GenerateRegistrationDto { - @ApiProperty({ example: 'abc@vomoto.com' }) - @IsNotEmpty({ message: 'Email is required.' }) - @IsEmail() - userName: string; + email: string; @IsOptional() @ApiProperty({ example: 'false' }) @@ -66,10 +63,10 @@ class ResponseDto { @ApiProperty() @IsString() - userName: string; + email: string; } -// + class VerifyAuthenticationResponseDto { @ApiProperty() @IsString() @@ -119,10 +116,7 @@ class VerifyAuthenticationResponseDto { export class VerifyAuthenticationPayloadDto { @ApiProperty() verifyAuthenticationDetails: VerifyAuthenticationDto; - - @ApiProperty() - @IsString() - userName: string; + email: string; } export class UpdateFidoUserDetailsDto { @@ -142,7 +136,7 @@ class VerifyAuthenticationResponseDto { export class UserNameDto { @ApiProperty() @IsString() - userName: string; + email: string; } export class credentialDto { diff --git a/apps/user/src/fido/fido.controller.ts b/apps/user/src/fido/fido.controller.ts index 6d3f9f4dd..9d43dbf0d 100644 --- a/apps/user/src/fido/fido.controller.ts +++ b/apps/user/src/fido/fido.controller.ts @@ -34,7 +34,7 @@ export class FidoController { */ @MessagePattern({ cmd: 'generate-authentication-options' }) generateAuthenticationOption(payload: GenerateRegistrationDto): Promise { - return this.fidoService.generateAuthenticationOption(payload.userName); + return this.fidoService.generateAuthenticationOption(payload.email); } /** * Description: FIDO User Verification @@ -61,7 +61,7 @@ export class FidoController { */ @MessagePattern({ cmd: 'fetch-fido-user-details' }) fetchFidoUserDetails(payload: UserNameDto):Promise { - return this.fidoService.fetchFidoUserDetails(payload.userName); + return this.fidoService.fetchFidoUserDetails(payload.email); } /** diff --git a/apps/user/src/fido/fido.service.ts b/apps/user/src/fido/fido.service.ts index 5095826b0..f21a8a164 100644 --- a/apps/user/src/fido/fido.service.ts +++ b/apps/user/src/fido/fido.service.ts @@ -18,17 +18,17 @@ export class FidoService { ) { } async generateRegistration(payload: GenerateRegistrationDto): Promise { try { - const { userName, deviceFlag } = payload; - const fidoUser = await this.fidoUserRepository.checkFidoUserExist(userName); + const { email, deviceFlag } = payload; + const fidoUser = await this.fidoUserRepository.checkFidoUserExist(email); if (!fidoUser && !fidoUser.id) { throw new NotFoundException(ResponseMessages.user.error.notFound); } if (!fidoUser || true === deviceFlag || false === deviceFlag) { - const generatedOption = await this.generateRegistrationOption(userName); + const generatedOption = await this.generateRegistrationOption(email); return generatedOption; } else if (!fidoUser.isFidoVerified) { - const generatedOption = await this.updateUserRegistrationOption(userName); + const generatedOption = await this.updateUserRegistrationOption(email); return generatedOption; } else { throw new BadRequestException(ResponseMessages.fido.error.exists); @@ -39,13 +39,13 @@ export class FidoService { } } - generateRegistrationOption(userName: string): Promise { - const url = `${process.env.FIDO_API_ENDPOINT}/generate-registration-options/?userName=${userName}`; + generateRegistrationOption(email: string): Promise { + const url = `${process.env.FIDO_API_ENDPOINT}/generate-registration-options/?userName=${email}`; return this.commonService .httpGet(url, { headers: { 'Content-Type': 'application/json' } }) .then(async (response) => { const { user } = response; - const updateUser = await this.fidoUserRepository.updateUserDetails(userName, [ + const updateUser = await this.fidoUserRepository.updateUserDetails(email, [ {fidoUserId:user.id}, {username:user.name} ]); @@ -57,14 +57,14 @@ export class FidoService { }); } - updateUserRegistrationOption(userName: string): Promise { - const url = `${process.env.FIDO_API_ENDPOINT}/generate-registration-options/?userName=${userName}`; + updateUserRegistrationOption(email: string): Promise { + const url = `${process.env.FIDO_API_ENDPOINT}/generate-registration-options/?userName=${email}`; 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, [ + await this.fidoUserRepository.updateUserDetails(email, [ {fidoUserId:user.id}, {isFidoVerified:false} ]); @@ -74,17 +74,17 @@ export class FidoService { async verifyRegistration(verifyRegistrationDto: VerifyRegistrationPayloadDto): Promise { try { - const { verifyRegistrationDetails, userName } = verifyRegistrationDto; + const { verifyRegistrationDetails, email } = 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}]); + if (response?.verified && email) { + await this.fidoUserRepository.updateUserDetails(email, [{isFidoVerified:true}]); const credentialID = response.newDevice.credentialID.replace(/=*$/, ''); response.newDevice.credentialID = credentialID; - const getUser = await this.fidoUserRepository.checkFidoUserExist(userName); + const getUser = await this.fidoUserRepository.checkFidoUserExist(email); await this.userDevicesRepository.createMultiDevice(response?.newDevice, getUser.id); return response; } else { @@ -96,15 +96,15 @@ export class FidoService { } } - async generateAuthenticationOption(userName: string): Promise { + async generateAuthenticationOption(email: string): Promise { try { - const fidoUser = await this.fidoUserRepository.checkFidoUserExist(userName); + const fidoUser = await this.fidoUserRepository.checkFidoUserExist(email); 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); + credentialIds.push(iterator.devices['credentialID']); } } else { throw new BadRequestException(ResponseMessages.fido.error.deviceNotFound); @@ -124,8 +124,8 @@ export class FidoService { async verifyAuthentication(verifyAuthenticationDto: VerifyAuthenticationPayloadDto): Promise { try { - const { verifyAuthenticationDetails, userName } = verifyAuthenticationDto; - const fidoUser = await this.fidoUserRepository.checkFidoUserExist(userName); + const { verifyAuthenticationDetails, email } = verifyAuthenticationDto; + const fidoUser = await this.fidoUserRepository.checkFidoUserExist(email); 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 }; @@ -164,11 +164,13 @@ export class FidoService { 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); + const updateFidoUserDetails = JSON.stringify(updateFidoUserDetailsDto); + const updateFidoUser = await this.userDevicesRepository.updateDeviceByCredentialId(updateFidoUserDetailsDto.credentialId); + + if (updateFidoUser[0].id) { + await this.userDevicesRepository.addCredentialIdAndNameById(updateFidoUser[0].id, updateFidoUserDetails); + } if (updateFidoUser[0].id) { return 'User updated.'; @@ -182,9 +184,9 @@ export class FidoService { } } - async fetchFidoUserDetails(userName: string): Promise { + async fetchFidoUserDetails(email: string): Promise { try { - const fidoUser = await this.fidoUserRepository.checkFidoUserExist(userName); + const fidoUser = await this.fidoUserRepository.checkFidoUserExist(email); if (!fidoUser) { throw new NotFoundException(ResponseMessages.user.error.notFound); } @@ -219,7 +221,6 @@ export class FidoService { 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) { diff --git a/apps/user/src/user.controller.ts b/apps/user/src/user.controller.ts index 629bb9dcf..026da028c 100644 --- a/apps/user/src/user.controller.ts +++ b/apps/user/src/user.controller.ts @@ -1,3 +1,4 @@ +import { AddPasskeyDetails, UpdateUserProfile, UserEmailVerificationDto, UserI, userInfo } from '../interfaces/user.interface'; import { AcceptRejectInvitationDto } from '../dtos/accept-reject-invitation.dto'; import { Controller } from '@nestjs/common'; @@ -5,8 +6,6 @@ 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 { AddPasskeyDetails, UpdateUserProfile, UserEmailVerificationDto, userInfo } from '../interfaces/user.interface'; - @Controller() export class UserController { @@ -43,7 +42,7 @@ export class UserController { } @MessagePattern({ cmd: 'get-user-public-profile' }) - async getPublicProfile(payload: { id }): Promise { + async getPublicProfile(payload: { username }): Promise { return this.userService.getPublicProfile(payload); } @MessagePattern({ cmd: 'update-user-profile' }) @@ -51,7 +50,7 @@ export class UserController { return this.userService.updateUserProfile(payload.updateUserProfileDto); } - @MessagePattern({ cmd: 'get-user-by-supabase' }) + @MessagePattern({ cmd: 'get-user-by-supabase' }) async findSupabaseUser(payload: { id }): Promise { return this.userService.findSupabaseUser(payload); } @@ -59,7 +58,7 @@ export class UserController { @MessagePattern({ cmd: 'get-user-by-mail' }) async findUserByEmail(payload: { email }): Promise { - return this.userService.findUserByEmail(payload); + return this.userService.findUserByEmail(payload); } @MessagePattern({ cmd: 'get-org-invitations' }) @@ -85,29 +84,29 @@ export class UserController { * @param payload * @returns organization users list */ - @MessagePattern({ cmd: 'fetch-organization-users' }) + @MessagePattern({ cmd: 'fetch-organization-user' }) async getOrganizationUsers(payload: { orgId: number, pageNumber: number, pageSize: number, search: string }): Promise { return this.userService.getOrgUsers(payload.orgId, payload.pageNumber, payload.pageSize, payload.search); } - /** - * - * @param payload - * @returns organization users list - */ - @MessagePattern({ cmd: 'fetch-users' }) - async get(payload: { pageNumber: number, pageSize: number, search: string }): Promise { - const users = this.userService.get(payload.pageNumber, payload.pageSize, payload.search); - return users; - } + /** + * + * @param payload + * @returns organization users list + */ + @MessagePattern({ cmd: 'fetch-users' }) + async get(payload: { pageNumber: number, pageSize: number, search: string }): Promise { + const users = this.userService.get(payload.pageNumber, payload.pageSize, payload.search); + return users; + } @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.createUserForToken(payload.userEmail, payload.userInfo); + async addUserDetailsInKeyCloak(payload: { userInfo: userInfo }): Promise { + return this.userService.createUserForToken(payload.userInfo); } // Fetch Users recent activities diff --git a/apps/user/src/user.service.ts b/apps/user/src/user.service.ts index f18f55843..b85d8df94 100644 --- a/apps/user/src/user.service.ts +++ b/apps/user/src/user.service.ts @@ -27,12 +27,12 @@ import { sendEmail } from '@credebl/common/send-grid-helper-file'; import { user } from '@prisma/client'; import { Inject } from '@nestjs/common'; import { HttpException } from '@nestjs/common'; -import { AddPasskeyDetails, InvitationsI, UpdateUserProfile, UserEmailVerificationDto, userInfo } from '../interfaces/user.interface'; +import { AddPasskeyDetails, InvitationsI, UpdateUserProfile, UserEmailVerificationDto, UserI, userInfo } from '../interfaces/user.interface'; import { AcceptRejectInvitationDto } from '../dtos/accept-reject-invitation.dto'; import { UserActivityService } from '@credebl/user-activity'; import { SupabaseService } from '@credebl/supabase'; import { UserDevicesRepository } from '../repositories/user-device.repository'; - +import { v4 as uuidv4 } from 'uuid'; @Injectable() export class UserService { @@ -58,7 +58,7 @@ export class UserService { 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); } @@ -67,7 +67,10 @@ export class UserService { throw new ConflictException(ResponseMessages.user.error.verificationAlreadySent); } - const resUser = await this.userRepository.createUser(userEmailVerificationDto); + const verifyCode = uuidv4(); + const uniqueUsername = await this.createUsername(userEmailVerificationDto.email, verifyCode); + userEmailVerificationDto.username = uniqueUsername; + const resUser = await this.userRepository.createUser(userEmailVerificationDto, verifyCode); try { await this.sendEmailForVerification(userEmailVerificationDto.email, resUser.verificationCode); @@ -78,7 +81,31 @@ export class UserService { return resUser; } catch (error) { this.logger.error(`In Create User : ${JSON.stringify(error)}`); - throw new RpcException(error.response); + throw new RpcException(error.response ? error.response : error); + } + } + + async createUsername(email: string, verifyCode: string): Promise { + + try { + // eslint-disable-next-line prefer-destructuring + const emailTrim = email.split('@')[0]; + + // Replace special characters with hyphens + const cleanedUsername = emailTrim.toLowerCase().replace(/[^a-zA-Z0-9_]/g, '-'); + + // Generate a 5-digit UUID + // eslint-disable-next-line prefer-destructuring + const uuid = verifyCode.split('-')[0]; + + // Combine cleaned username and UUID + const uniqueUsername = `${cleanedUsername}-${uuid}`; + + return uniqueUsername; + + } catch (error) { + this.logger.error(`Error in createUsername: ${JSON.stringify(error)}`); + throw new RpcException(error.response ? error.response : error); } } @@ -110,7 +137,7 @@ export class UserService { } catch (error) { this.logger.error(`Error in sendEmailForVerification: ${JSON.stringify(error)}`); - throw new InternalServerErrorException(error.message); + throw new RpcException(error.response ? error.response : error); } } @@ -147,17 +174,19 @@ export class UserService { } } catch (error) { this.logger.error(`error in verifyEmail: ${JSON.stringify(error)}`); - throw new RpcException(error.response); + throw new RpcException(error.response ? error.response : error); } } - async createUserForToken(email: string, userInfo: userInfo): Promise { + async createUserForToken(userInfo: userInfo): Promise { try { - if (!email) { + const { email } = userInfo; + if (!userInfo.email) { throw new UnauthorizedException(ResponseMessages.user.error.invalidEmail); } - const checkUserDetails = await this.userRepository.getUserDetails(email); + const checkUserDetails = await this.userRepository.getUserDetails(userInfo.email); + if (!checkUserDetails) { throw new NotFoundException(ResponseMessages.user.error.invalidEmail); } @@ -167,12 +196,11 @@ export class UserService { if (false === checkUserDetails.isEmailVerified) { throw new NotFoundException(ResponseMessages.user.error.verifyEmail); } - const resUser = await this.userRepository.updateUserInfo(email, userInfo); + const resUser = await this.userRepository.updateUserInfo(userInfo.email, userInfo); if (!resUser) { throw new NotFoundException(ResponseMessages.user.error.invalidEmail); } - const userDetails = await this.userRepository.getUserDetails(email); - + const userDetails = await this.userRepository.getUserDetails(userInfo.email); if (!userDetails) { throw new NotFoundException(ResponseMessages.user.error.adduser); } @@ -180,7 +208,7 @@ export class UserService { let supaUser; if (userInfo.isPasskey) { - const resUser = await this.userRepository.addUserPassword(email, userInfo.password); + const resUser = await this.userRepository.addUserPassword(email, userInfo.password); const userDetails = await this.userRepository.getUserDetails(email); const decryptedPassword = await this.commonService.decryptPassword(userDetails.password); if (!resUser) { @@ -215,7 +243,7 @@ export class UserService { return 'User created successfully'; } catch (error) { this.logger.error(`Error in createUserForToken: ${JSON.stringify(error)}`); - throw new RpcException(error.response); + throw new RpcException(error.response ? error.response : error); } } @@ -242,7 +270,7 @@ export class UserService { return 'User updated successfully'; } catch (error) { this.logger.error(`Error in createUserForToken: ${JSON.stringify(error)}`); - throw new RpcException(error.response); + throw new RpcException(error.response ? error.response : error); } } @@ -254,10 +282,8 @@ export class UserService { */ 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); } @@ -276,46 +302,64 @@ export class UserService { return this.generateToken(email, decryptedPassword); } - return this.generateToken(email, password); + return this.generateToken(email, password); } catch (error) { this.logger.error(`In Login User : ${JSON.stringify(error)}`); - throw new RpcException(error.response); + throw new RpcException(error.response ? error.response : error); } } async generateToken(email: string, password: string): Promise { - const supaInstance = await this.supabaseService.getClient(); + try { + const supaInstance = await this.supabaseService.getClient(); - this.logger.error(`supaInstance::`, supaInstance); + this.logger.error(`supaInstance::`, supaInstance); - const { data, error } = await supaInstance.auth.signInWithPassword({ - email, - password - }); + const { data, error } = await supaInstance.auth.signInWithPassword({ + email, + password + }); + this.logger.error(`Supa Login Error::`, JSON.stringify(error)); - this.logger.error(`Supa Login Error::`, JSON.stringify(error)); + if (error) { + throw new BadRequestException(error?.message); + } - if (error) { - throw new BadRequestException(error?.message); + const token = data?.session; + return token; + } catch (error) { + throw new RpcException(error.response ? error.response : error); } - - const token = data?.session; - - return token; } async getProfile(payload: { id }): Promise { try { - return this.userRepository.getUserById(payload.id); + const userData = await this.userRepository.getUserById(payload.id); + const ecosystemDetails = await this.prisma.ecosystem_config.findFirst( + { + where:{ + key: 'enableEcosystem' + } + } + ); + + if ('true' === ecosystemDetails.value) { + userData['enableEcosystem'] = true; + return userData; + } + + userData['enableEcosystem'] = false; + return userData; + } catch (error) { this.logger.error(`get user: ${JSON.stringify(error)}`); - throw new RpcException(error.response); + throw new RpcException(error.response ? error.response : error); } } - async getPublicProfile(payload: { id }): Promise { + async getPublicProfile(payload: { username }): Promise { try { - const userProfile = await this.userRepository.getUserPublicProfile(payload.id); + const userProfile = await this.userRepository.getUserPublicProfile(payload.username); if (!userProfile) { throw new NotFoundException(ResponseMessages.user.error.profileNotFound); @@ -324,16 +368,16 @@ export class UserService { return userProfile; } catch (error) { this.logger.error(`get user: ${JSON.stringify(error)}`); - throw new RpcException(error.response); + throw new RpcException(error.response ? error.response : error); } } - async updateUserProfile(updateUserProfileDto: UpdateUserProfile): Promise { + async updateUserProfile(updateUserProfileDto: UpdateUserProfile): Promise { try { return this.userRepository.updateUserProfile(updateUserProfileDto); } catch (error) { this.logger.error(`update user profile: ${JSON.stringify(error)}`); - throw new RpcException(error.response); + throw new RpcException(error.response ? error.response : error); } } @@ -342,25 +386,25 @@ export class UserService { return this.userRepository.getUserBySupabaseId(payload.id); } catch (error) { this.logger.error(`get user: ${JSON.stringify(error)}`); - throw new RpcException(error.response); + throw new RpcException(error.response ? error.response : error); } } async findSupabaseUser(payload: { id }): Promise { try { - return this.userRepository.getUserBySupabaseId(payload.id); + return await this.userRepository.getUserBySupabaseId(payload.id); } catch (error) { this.logger.error(`Error in findSupabaseUser: ${JSON.stringify(error)}`); - throw new RpcException(error.response); + throw new RpcException(error.response ? error.response : error); } } async findUserByEmail(payload: { email }): Promise { try { - return this.userRepository.findUserByEmail(payload.email); + return await this.userRepository.findUserByEmail(payload.email); } catch (error) { this.logger.error(`findUserByEmail: ${JSON.stringify(error)}`); - throw new RpcException(error.response); + throw new RpcException(error.response ? error.response : error); } } @@ -385,7 +429,7 @@ export class UserService { return invitationsData; } catch (error) { this.logger.error(`Error in get invitations: ${JSON.stringify(error)}`); - throw new RpcException(error.response); + throw new RpcException(error.response ? error.response : error); } } @@ -445,7 +489,7 @@ export class UserService { return this.fetchInvitationsStatus(acceptRejectInvitation, userId, userData.email); } catch (error) { this.logger.error(`acceptRejectInvitations: ${error}`); - throw new RpcException(error.response); + throw new RpcException(error.response ? error.response : error); } } @@ -485,7 +529,7 @@ export class UserService { return invitationsData; } catch (error) { this.logger.error(`Error In fetchInvitationsStatus: ${JSON.stringify(error)}`); - throw new RpcException(error.response); + throw new RpcException(error.response ? error.response : error); } } @@ -496,6 +540,7 @@ export class UserService { */ async getOrgUsers(orgId: number, pageNumber: number, pageSize: number, search: string): Promise { try { + const query = { userOrgRoles: { some: { orgId } @@ -510,10 +555,11 @@ export class UserService { const filterOptions = { orgId }; + return this.userRepository.findOrgUsers(query, pageNumber, pageSize, filterOptions); } catch (error) { this.logger.error(`get Org Users: ${JSON.stringify(error)}`); - throw new RpcException(error.response); + throw new RpcException(error.response ? error.response : error); } } @@ -535,7 +581,7 @@ export class UserService { return this.userRepository.findUsers(query, pageNumber, pageSize); } catch (error) { this.logger.error(`get Users: ${JSON.stringify(error)}`); - throw new RpcException(error.response); + throw new RpcException(error.response ? error.response : error); } } @@ -548,19 +594,22 @@ export class UserService { } else if (userDetails && userDetails.supabaseUserId) { throw new ConflictException(ResponseMessages.user.error.exists); } else if (null === userDetails) { - return 'New User'; + return { + isExist: false + }; } else { const userVerificationDetails = { isEmailVerified: userDetails.isEmailVerified, isFidoVerified: userDetails.isFidoVerified, - isSupabase: null !== userDetails.supabaseUserId && undefined !== userDetails.supabaseUserId + isSupabase: null !== userDetails.supabaseUserId && undefined !== userDetails.supabaseUserId, + isExist: true }; return userVerificationDetails; } } catch (error) { this.logger.error(`In check User : ${JSON.stringify(error)}`); - throw new RpcException(error.response); + throw new RpcException(error.response ? error.response : error); } } @@ -572,7 +621,7 @@ export class UserService { } catch (error) { this.logger.error(`In getUserActivity : ${JSON.stringify(error)}`); - throw new RpcException(error.response); + throw new RpcException(error.response ? error.response : error); } } -} +} \ No newline at end of file diff --git a/apps/verification/src/verification.service.ts b/apps/verification/src/verification.service.ts index c0b57c04f..b7fbad1e0 100644 --- a/apps/verification/src/verification.service.ts +++ b/apps/verification/src/verification.service.ts @@ -49,7 +49,7 @@ export class VerificationService { } catch (error) { this.logger.error(`[getProofPresentations] - error in get proof presentation : ${JSON.stringify(error)}`); - throw new RpcException(error); + throw new RpcException(error.response ? error.response : error); } } @@ -109,7 +109,7 @@ export class VerificationService { return getProofPresentationById?.response; } catch (error) { this.logger.error(`[getProofPresentationById] - error in get proof presentation by id : ${JSON.stringify(error)}`); - throw new RpcException(error); + throw new RpcException(error.response ? error.response : error); } } @@ -157,8 +157,6 @@ export class VerificationService { */ async sendProofRequest(requestProof: IRequestProof): Promise { try { - let requestedAttributes = {}; - const requestedPredicates = {}; const comment = requestProof.comment ? requestProof.comment : ''; let proofRequestPayload: ISendProofRequestPayload = { @@ -176,73 +174,7 @@ export class VerificationService { autoAcceptProof: '' }; - const attributeWithSchemaIdExists = requestProof.attributes.some(attribute => attribute.schemaId); - if (attributeWithSchemaIdExists) { - requestedAttributes = Object.fromEntries(requestProof.attributes.map((attribute, index) => { - - const attributeElement = attribute.attributeName; - const attributeReferent = `additionalProp${index + 1}`; - - if (!attribute.condition && !attribute.value) { - const keys = Object.keys(requestedAttributes); - - if (0 < keys.length) { - let attributeFound = false; - - for (const attr of keys) { - if ( - requestedAttributes[attr].restrictions.some(res => res.schema_id) === - requestProof.attributes[index].schemaId - ) { - requestedAttributes[attr].name.push(attributeElement); - attributeFound = true; - } - - if (attr === keys[keys.length - 1] && !attributeFound) { - requestedAttributes[attributeReferent] = { - name: attributeElement, - restrictions: [ - { - cred_def_id: requestProof.attributes[index].credDefId ? requestProof.attributes[index].credDefId : undefined, - schema_id: requestProof.attributes[index].schemaId - } - ] - }; - } - } - } else { - return [ - attributeReferent, - { - name: attributeElement, - restrictions: [ - { - cred_def_id: requestProof.attributes[index].credDefId ? requestProof.attributes[index].credDefId : undefined, - schema_id: requestProof.attributes[index].schemaId - } - ] - } - ]; - } - } else { - requestedPredicates[attributeReferent] = { - p_type: attribute.condition, - restrictions: [ - { - cred_def_id: requestProof.attributes[index].credDefId ? requestProof.attributes[index].credDefId : undefined, - schema_id: requestProof.attributes[index].schemaId - } - ], - name: attributeElement, - p_value: parseInt(attribute.value) - }; - } - - return [attributeReferent]; - })); - } else { - throw new BadRequestException(ResponseMessages.verification.error.schemaIdNotFound); - } + const { requestedAttributes, requestedPredicates } = await this._proofRequestPayload(requestProof); proofRequestPayload = { protocolVersion: requestProof.protocolVersion ? requestProof.protocolVersion : 'v1', @@ -272,7 +204,7 @@ export class VerificationService { return getProofPresentationById?.response; } catch (error) { this.logger.error(`[verifyPresentation] - error in verify presentation : ${JSON.stringify(error)}`); - throw new RpcException(error); + throw new RpcException(error.response ? error.response : error); } } @@ -330,7 +262,7 @@ export class VerificationService { return getProofPresentationById?.response; } catch (error) { this.logger.error(`[verifyPresentation] - error in verify presentation : ${JSON.stringify(error)}`); - throw new RpcException(error); + throw new RpcException(error.response ? error.response : error); } } @@ -378,7 +310,7 @@ export class VerificationService { } catch (error) { this.logger.error(`[webhookProofPresentation] - error in webhook proof presentation : ${JSON.stringify(error)}`); - throw new RpcException(error); + throw new RpcException(error.response ? error.response : error); } } @@ -487,7 +419,7 @@ export class VerificationService { } catch (error) { this.logger.error(`[sendOutOfBandPresentationRequest] - error in out of band proof request : ${JSON.stringify(error)}`); - throw new RpcException(error); + throw new RpcException(error.response ? error.response : error); } } @@ -536,9 +468,8 @@ export class VerificationService { const requestedPredicates = {}; const attributeWithSchemaIdExists = proofRequestpayload.attributes.some(attribute => attribute.schemaId); if (attributeWithSchemaIdExists) { + requestedAttributes = Object.fromEntries(proofRequestpayload.attributes.map((attribute, index) => { - requestedAttributes = {}; - for (const [index, attribute] of proofRequestpayload.attributes.entries()) { const attributeElement = attribute.attributeName; const attributeReferent = `additionalProp${index + 1}`; @@ -550,9 +481,8 @@ export class VerificationService { for (const attr of keys) { if ( - requestedAttributes[attr].restrictions.some( - res => res.schema_id === proofRequestpayload.attributes[index].schemaId - ) + requestedAttributes[attr].restrictions.some(res => res.schema_id) === + proofRequestpayload.attributes[index].schemaId ) { requestedAttributes[attr].name.push(attributeElement); attributeFound = true; @@ -563,9 +493,7 @@ export class VerificationService { name: attributeElement, restrictions: [ { - cred_def_id: proofRequestpayload.attributes[index].credDefId - ? proofRequestpayload.attributes[index].credDefId - : undefined, + cred_def_id: proofRequestpayload.attributes[index].credDefId ? proofRequestpayload.attributes[index].credDefId : undefined, schema_id: proofRequestpayload.attributes[index].schemaId } ] @@ -573,32 +501,25 @@ export class VerificationService { } } } else { - requestedAttributes[attributeReferent] = { - name: attributeElement, - restrictions: [ - { - cred_def_id: proofRequestpayload.attributes[index].credDefId - ? proofRequestpayload.attributes[index].credDefId - : undefined, - schema_id: proofRequestpayload.attributes[index].schemaId - } - ] - }; + return [ + attributeReferent, + { + name: attributeElement, + restrictions: [ + { + cred_def_id: proofRequestpayload.attributes[index].credDefId ? proofRequestpayload.attributes[index].credDefId : undefined, + schema_id: proofRequestpayload.attributes[index].schemaId + } + ] + } + ]; } } else { - if (isNaN(parseInt(attribute.value))) { - throw new BadRequestException( - ResponseMessages.verification.error.predicatesValueNotNumber - ); - } - requestedPredicates[attributeReferent] = { p_type: attribute.condition, restrictions: [ { - cred_def_id: proofRequestpayload.attributes[index].credDefId - ? proofRequestpayload.attributes[index].credDefId - : undefined, + cred_def_id: proofRequestpayload.attributes[index].credDefId ? proofRequestpayload.attributes[index].credDefId : undefined, schema_id: proofRequestpayload.attributes[index].schemaId } ], @@ -606,20 +527,20 @@ export class VerificationService { p_value: parseInt(attribute.value) }; } - } + + return [attributeReferent]; + })); return { requestedAttributes, requestedPredicates }; } else { - throw new BadRequestException( - ResponseMessages.verification.error.schemaIdNotFound - ); + throw new BadRequestException(ResponseMessages.verification.error.schemaIdNotFound); } } catch (error) { this.logger.error(`[proofRequestPayload] - error in proof request payload : ${JSON.stringify(error)}`); - throw new RpcException(error); + throw new RpcException(error.response ? error.response : error); } } @@ -815,7 +736,7 @@ export class VerificationService { return extractedDataArray; } catch (error) { this.logger.error(`[getProofFormData] - error in get proof form data : ${JSON.stringify(error)}`); - throw new RpcException(error); + throw new RpcException(error.response ? error.response : error); } } diff --git a/libs/common/src/common.constant.ts b/libs/common/src/common.constant.ts index 01b366bd6..2e9fe6e88 100644 --- a/libs/common/src/common.constant.ts +++ b/libs/common/src/common.constant.ts @@ -83,6 +83,10 @@ export enum CommonConstants { // SHARED AGENT URL_SHAGENT_CREATE_TENANT = '/multi-tenancy/create-tenant', URL_SHAGENT_WITH_TENANT_AGENT = '/multi-tenancy/with-tenant-agent', + URL_SHAGENT_CREATE_SCHEMA = '/multi-tenancy/schema/#', + URL_SHAGENT_GET_SCHEMA = '/multi-tenancy/schema/@/#', + URL_SHAGENT_CREATE_CRED_DEF = '/multi-tenancy/credential-definition/#', + URL_SHAGENT_GET_CRED_DEF = '/multi-tenancy/credential-definition/@/#', URL_SHAGENT_CREATE_INVITATION = '/multi-tenancy/create-legacy-invitation/#', URL_SHAGENT_GET_CREATEED_INVITATIONS = '/multi-tenancy/connections/#', URL_SHAGENT_GET_CREATEED_INVITATION_BY_CONNECTIONID = '/multi-tenancy/connections/#/@', @@ -257,6 +261,8 @@ export enum CommonConstants { ONBOARDING_TYPE_EXTERNAL = 1, ONBOARDING_TYPE_INVITATION = 2, + // ecosystem config auto endorsement + ECOSYSTEM_AUTO_ENDOSEMENT = 'autoEndorsement', // Network TESTNET = 'testnet', @@ -277,6 +283,15 @@ export enum CommonConstants { ENDORSER_DID = 8, ORGANIZATION_CREATION = 9, ADD_USER = 10, + + // Ecosystem + SIGN_TRANSACTION = '/transactions/endorse', + SUBMIT_TRANSACTION = '/transactions/write', + TRANSACTION_MULTITENANT_SCHEMA = '/multi-tenancy/schema/#', + TRANSACTION_MULTITENANT_CRED_DEF = '/multi-tenancy/credential-definition/#', + TRANSACTION_MULTITENANT_SIGN = '/multi-tenancy/transactions/endorse/#', + TRANSACTION_MULTITENANT_SUMBIT = '/multi-tenancy/transactions/write/#' + } export const postgresqlErrorCodes = []; diff --git a/libs/common/src/response-messages/index.ts b/libs/common/src/response-messages/index.ts index 1d265cf7a..6faf17c0b 100644 --- a/libs/common/src/response-messages/index.ts +++ b/libs/common/src/response-messages/index.ts @@ -13,14 +13,15 @@ export const ResponseMessages = { 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' + sendVerificationCode: 'Verification code has been sent sucessfully to the mail. Please verify', + userActivity: 'User activities fetched successfully' }, error: { exists: 'User already exists', profileNotFound: 'User public profile not found', 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!', + invalidEmailUrl: 'Invalid verification code or EmailId!', verifiedEmail: 'Email already verified', notFound: 'User not found', verifyMail: 'Please verify your email', @@ -32,7 +33,8 @@ export const ResponseMessages = { invalidEmail: 'Invalid Email Id!', adduser: 'Unable to add user details', verifyEmail: 'The verification link has already been sent to your email address. please verify', - emailNotVerified: 'The verification link has already been sent to your email address. please verify' + emailNotVerified: 'The verification link has already been sent to your email address. please verify', + userNotRegisterd: 'The user has not yet completed the registration process' } }, organisation: { @@ -41,7 +43,7 @@ export const ResponseMessages = { update: 'Organization updated successfully', fetchProfile: 'Organization profile fetched successfully', fetchOrgRoles: 'Organization roles fetched successfully', - createInvitation: 'Organization invitations sent successfully', + createInvitation: 'Organization invitations sent', getInvitation: 'Organization invitations fetched successfully', getOrganization: 'Organization details fetched successfully', getOrgDashboard: 'Organization dashboard details fetched', @@ -113,9 +115,17 @@ export const ResponseMessages = { credDefIdNotFound: 'Credential Definition Id not found' } }, + ledger: { + success: { + fetch: 'Ledgers retrieved successfully.' + }, + error: { + NotFound: 'No ledgers found.' + } + }, agent: { success: { - create: 'Agent spin-up up successfully', + create: 'Agent spin-up successfully', health: 'Agent health details retrieved successfully.' }, error: { @@ -131,7 +141,7 @@ export const ResponseMessages = { connection: { success: { create: 'Connection created successfully', - fetch: 'Connection Details fetched successfully' + fetch: 'Connection fetched successfully' }, error: { exists: 'Connection is already exist', @@ -171,5 +181,60 @@ export const ResponseMessages = { platformConfigNotFound: 'Platform config not found', emailSend: 'Unable to send email to the user' } + }, + ecosystem: { + success: { + create: 'Ecosystem created successfully', + update: 'Ecosystem updated successfully', + delete: 'Ecosystem invitations deleted successfully', + fetch: 'Ecosystem fetched successfully', + getEcosystemDashboard: 'Ecosystem dashboard details fetched successfully', + getInvitation: 'Ecosystem invitations fetched successfully', + createInvitation: 'Ecosystem invitations sent', + schemaRequest: 'Schema transaction request created successfully', + credDefRequest: 'credential-definition transaction request created successfully', + sign: 'Transaction request signed successfully', + submit: 'Transaction request submitted successfully', + invitationReject: 'Ecosystem invitation rejected', + invitationAccept: 'Ecosystem invitation accepted successfully', + fetchEndorsors: 'Endorser transactions fetched successfully', + DeclineEndorsementTransaction: 'Decline endorsement request successfully', + AutoEndorsementTransaction: 'The flag for transactions has been successfully set', + fetchMembers: 'Ecosystem members fetched successfully' + }, + error: { + notCreated: 'Error while creating ecosystem', + update: 'Error while updating ecosystem', + invalidInvitationStatus: 'Invalid invitation status', + invitationNotFound: 'Ecosystem Invitation not found', + invitationNotUpdate: 'Ecosystem Invitation not updated', + orgsNotUpdate: 'Ecosystem Orgs not updated', + ecosystemNotEnabled: 'Ecosystem service is not enabled', + sumbitTransaction: 'Error while submitting transaction', + requestSchemaTransaction: 'Error while request schema transaction', + requestCredDefTransaction: 'Error while submitting transaction', + notFound: 'Organization not found', + platformConfigNotFound: 'Platform configurations not found', + schemaNotFound: 'Schema not found', + ecosystemNotFound: 'Ecosystem not found', + ecosystemOrgNotFound: 'Ecosystem org not found', + ecosystemConfigNotFound: 'Ecosystem config not found', + credentialDefinitionNotFound: 'Credential definition found', + leadNotFound: 'Lead details not found', + signRequestError: 'Error while signing the transaction', + updateTransactionError: 'Error while update the transaction', + schemaAlreadyExist: 'Schema name and schema version already exist', + credDefAlreadyExist: 'Credential definition already exist', + saveSchema: 'Error while storing the schema details', + saveCredDef: 'Error while storing the credential-definition details', + invalidOrgId: 'Invalid organization Id', + invalidEcosystemId: 'Invalid ecosystem Id', + invalidTransaction: 'Transaction does not exist', + transactionSubmitted: 'Transaction already submitted', + invalidAgentUrl: 'Invalid agent url', + EndorsementTransactionNotFoundException: 'Endorsement transaction with status requested not found', + OrgOrEcosystemNotFoundExceptionForEndorsementTransaction: 'The endorsement transaction status cant be updated', + ecosystemOrgAlready: 'Organization is already part of the ecosystem. Please ensure that the organization is not duplicated.' + } } -}; +}; \ No newline at end of file diff --git a/libs/common/src/send-grid-helper-file.ts b/libs/common/src/send-grid-helper-file.ts index a98bd518b..623d118e1 100644 --- a/libs/common/src/send-grid-helper-file.ts +++ b/libs/common/src/send-grid-helper-file.ts @@ -18,7 +18,7 @@ export const sendEmail = async (EmailDto: EmailDto): Promise => { html: EmailDto.emailHtml, attachments: EmailDto.emailAttachments }; - return await sendgrid.send(msg).then(() => true).catch(() => false) + return await sendgrid.send(msg).then(() => true).catch(() => false); } catch (error) { return false; diff --git a/libs/enum/src/enum.ts b/libs/enum/src/enum.ts index a59e0c81e..e47471927 100644 --- a/libs/enum/src/enum.ts +++ b/libs/enum/src/enum.ts @@ -13,6 +13,17 @@ export enum Invitation { PENDING = 'pending' } +export enum EcosystemRoles { + ECOSYSTEM_LEAD = 'Ecosystem Lead', + ECOSYSTEM_MEMBER = 'Ecosystem Member', + ECOSYSTEM_OWNER = 'Ecosystem Owner' +} + +export enum EndorserTransactionType{ + SCHEMA = 'schema', + CREDENTIAL_DEFINITION = 'credential-definition', +} + export enum OrgAgentType { DEDICATED = 1, SHARED = 2 diff --git a/libs/prisma-service/prisma/data/credebl-master-table.json b/libs/prisma-service/prisma/data/credebl-master-table.json index 35a44c6c3..23bbb73b2 100644 --- a/libs/prisma-service/prisma/data/credebl-master-table.json +++ b/libs/prisma-service/prisma/data/credebl-master-table.json @@ -55,6 +55,21 @@ "description": "Joins the organization as member" } ], + "ecosystemRoleData": [ + { + "name": "Ecosystem Owner", + "description": "Ecosystem Owner" + }, + { + "name": "Ecosystem Lead", + "description": "Ecosystem Lead" + }, + { + "name": "Ecosystem Member", + "description": "Ecosystem Member" + } + ], + "agentTypeData": [ { "agent": "AFJ" @@ -79,7 +94,74 @@ "isActive": true, "networkString": "testnet", "registerDIDEndpoint": "http://test.bcovrin.vonx.io/register", - "registerDIDPayload": "" + "registerDIDPayload": "", + "indyNamespace": "bcovrin:testnet" + }, + { + "name": "Indicio Testnet", + "networkType": "testnet", + "poolConfig": "https://raw.githubusercontent.com/Indicio-tech/indicio-network/main/genesis_files/pool_transactions_testnet_genesis", + "isActive": true, + "networkString": "testnet", + "registerDIDEndpoint": "https://selfserve.indiciotech.io/nym", + "registerDIDPayload": "", + "indyNamespace": "indicio:testnet" + } + ], + "endorseData": [ + { + "id": "0f8fad5b-d9cb-469f-a165-70867728950f", + "endorserDid": "endorser123", + "authorDid": "author456", + "requestPayload": "{\"type\": \"dummy_request_1\"}", + "responsePayload": "{\"type\": \"dummy_response_1\"}", + "status": "Requested", + "ecosystemOrgId": "1c247b4a-e2f6-48c0-8aa2-65ea47474294" + }, + { + "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", + "endorserDid": "endorser789", + "authorDid": "author101", + "requestPayload": "{\"type\": \"dummy_request_2\"}", + "responsePayload": "{\"type\": \"dummy_response_2\"}", + "status": "Signed", + "ecosystemOrgId": "1c247b4a-e2f6-48c0-8aa2-65ea47474294" + }, + { + "id": "a89b6e81-a1ff-4d13-a9e2-17176e707aac", + "endorserDid": "endorser321", + "authorDid": "author654", + "requestPayload": "{\"type\": \"dummy_request_3\"}", + "responsePayload": "{\"type\": \"dummy_response_3\"}", + "status": "Declined", + "ecosystemOrgId": "a2443e09-45be-4739-b8b3-0d4ffaecea94" + }, + { + "id": "f47ac10b-58cc-4372-a567-0e02b2c3d47c", + "endorserDid": "endorser999", + "authorDid": "author777", + "requestPayload": "{\"type\": \"dummy_request_4\"}", + "responsePayload": "{\"type\": \"dummy_response_4\"}", + "status": "Submitted", + "ecosystemOrgId": "ca6ee687-a3a9-42ce-9e49-02bf62f5c93a" + } + ], + "ecosystemConfigData": [ + { + "key": "url", + "value": "https://dummyurl.tk" + }, + { + "key": "enableEcosystem", + "value": "false" + }, + { + "key": "autoEndorsement", + "value": "false" + }, + { + "key": "participateInEcosystem", + "value": "false" } ] } \ No newline at end of file diff --git a/libs/prisma-service/prisma/migrations/20230928140558_ledgers_indy_namespace/migration.sql b/libs/prisma-service/prisma/migrations/20230928140558_ledgers_indy_namespace/migration.sql new file mode 100644 index 000000000..caa16612f --- /dev/null +++ b/libs/prisma-service/prisma/migrations/20230928140558_ledgers_indy_namespace/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "ledgers" ADD COLUMN "indyNamespace" VARCHAR; diff --git a/libs/prisma-service/prisma/migrations/20231002114401_ecosystem_setup/migration.sql b/libs/prisma-service/prisma/migrations/20231002114401_ecosystem_setup/migration.sql new file mode 100644 index 000000000..0f1eed81d --- /dev/null +++ b/libs/prisma-service/prisma/migrations/20231002114401_ecosystem_setup/migration.sql @@ -0,0 +1,90 @@ +-- CreateTable +CREATE TABLE "ecosystem_roles" ( + "id" SERIAL NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT NOT NULL, + "createDateTime" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "createdBy" INTEGER NOT NULL DEFAULT 1, + "lastChangedDateTime" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "lastChangedBy" INTEGER NOT NULL DEFAULT 1, + "deletedAt" TIMESTAMP(6), + + CONSTRAINT "ecosystem_roles_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "ecosystem" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT NOT NULL, + "tags" TEXT NOT NULL, + "createDateTime" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "createdBy" INTEGER NOT NULL DEFAULT 1, + "lastChangedDateTime" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "lastChangedBy" INTEGER NOT NULL DEFAULT 1, + "deletedAt" TIMESTAMP(6), + + CONSTRAINT "ecosystem_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "ecosystem_invitations" ( + "id" TEXT NOT NULL, + "email" TEXT NOT NULL, + "status" TEXT NOT NULL, + "ecosystemId" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "orgId" TEXT NOT NULL, + "createDateTime" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "createdBy" INTEGER NOT NULL DEFAULT 1, + "lastChangedDateTime" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "lastChangedBy" INTEGER NOT NULL DEFAULT 1, + "deletedAt" TIMESTAMP(6), + + CONSTRAINT "ecosystem_invitations_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "ecosystem_users" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "ecosystemId" TEXT NOT NULL, + "createDateTime" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "createdBy" INTEGER NOT NULL DEFAULT 1, + "lastChangedDateTime" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "lastChangedBy" INTEGER NOT NULL DEFAULT 1, + "deletedAt" TIMESTAMP(6), + + CONSTRAINT "ecosystem_users_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "ecosystem_orgs" ( + "id" TEXT NOT NULL, + "orgId" TEXT NOT NULL, + "status" TEXT NOT NULL, + "ecosystemId" TEXT NOT NULL, + "ecosystemRoleId" INTEGER NOT NULL, + "createDateTime" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "createdBy" INTEGER NOT NULL DEFAULT 1, + "lastChangedDateTime" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "lastChangedBy" INTEGER NOT NULL DEFAULT 1, + "deletedAt" TIMESTAMP(6), + + CONSTRAINT "ecosystem_orgs_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "ecosystem_roles_name_key" ON "ecosystem_roles"("name"); + +-- AddForeignKey +ALTER TABLE "ecosystem_invitations" ADD CONSTRAINT "ecosystem_invitations_ecosystemId_fkey" FOREIGN KEY ("ecosystemId") REFERENCES "ecosystem"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ecosystem_users" ADD CONSTRAINT "ecosystem_users_ecosystemId_fkey" FOREIGN KEY ("ecosystemId") REFERENCES "ecosystem"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ecosystem_orgs" ADD CONSTRAINT "ecosystem_orgs_ecosystemId_fkey" FOREIGN KEY ("ecosystemId") REFERENCES "ecosystem"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ecosystem_orgs" ADD CONSTRAINT "ecosystem_orgs_ecosystemRoleId_fkey" FOREIGN KEY ("ecosystemRoleId") REFERENCES "ecosystem_roles"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/libs/prisma-service/prisma/migrations/20231003080927_ecosystem_flag/migration.sql b/libs/prisma-service/prisma/migrations/20231003080927_ecosystem_flag/migration.sql new file mode 100644 index 000000000..418d645c7 --- /dev/null +++ b/libs/prisma-service/prisma/migrations/20231003080927_ecosystem_flag/migration.sql @@ -0,0 +1,24 @@ +/* + Warnings: + + - The primary key for the `ecosystem_roles` table will be changed. If it partially fails, the table could be left without primary key constraint. + +*/ +-- DropForeignKey +ALTER TABLE "ecosystem_orgs" DROP CONSTRAINT "ecosystem_orgs_ecosystemRoleId_fkey"; + +-- AlterTable +ALTER TABLE "ecosystem_orgs" ALTER COLUMN "ecosystemRoleId" SET DATA TYPE TEXT; + +-- AlterTable +ALTER TABLE "ecosystem_roles" DROP CONSTRAINT "ecosystem_roles_pkey", +ALTER COLUMN "id" DROP DEFAULT, +ALTER COLUMN "id" SET DATA TYPE TEXT, +ADD CONSTRAINT "ecosystem_roles_pkey" PRIMARY KEY ("id"); +DROP SEQUENCE "ecosystem_roles_id_seq"; + +-- AlterTable +ALTER TABLE "platform_config" ADD COLUMN "enableEcosystem" BOOLEAN NOT NULL DEFAULT false; + +-- AddForeignKey +ALTER TABLE "ecosystem_orgs" ADD CONSTRAINT "ecosystem_orgs_ecosystemRoleId_fkey" FOREIGN KEY ("ecosystemRoleId") REFERENCES "ecosystem_roles"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/libs/prisma-service/prisma/migrations/20231003114625_ecosystem_logo/migration.sql b/libs/prisma-service/prisma/migrations/20231003114625_ecosystem_logo/migration.sql new file mode 100644 index 000000000..d4031a104 --- /dev/null +++ b/libs/prisma-service/prisma/migrations/20231003114625_ecosystem_logo/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "ecosystem" ADD COLUMN "logoUrl" TEXT; diff --git a/libs/prisma-service/prisma/migrations/20231006120455_endorsement_transaction/migration.sql b/libs/prisma-service/prisma/migrations/20231006120455_endorsement_transaction/migration.sql new file mode 100644 index 000000000..083feb655 --- /dev/null +++ b/libs/prisma-service/prisma/migrations/20231006120455_endorsement_transaction/migration.sql @@ -0,0 +1,15 @@ +-- CreateTable +CREATE TABLE "endorsement_transaction" ( + "id" TEXT NOT NULL, + "endorserDid" TEXT NOT NULL, + "authorDid" TEXT NOT NULL, + "requestPayload" TEXT NOT NULL, + "responsePayload" TEXT NOT NULL, + "status" TEXT NOT NULL, + "ecosystemOrgId" TEXT NOT NULL, + + CONSTRAINT "endorsement_transaction_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "endorsement_transaction" ADD CONSTRAINT "endorsement_transaction_ecosystemOrgId_fkey" FOREIGN KEY ("ecosystemOrgId") REFERENCES "ecosystem_orgs"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/libs/prisma-service/prisma/migrations/20231007125030_endorse_transaction_type_date/migration.sql b/libs/prisma-service/prisma/migrations/20231007125030_endorse_transaction_type_date/migration.sql new file mode 100644 index 000000000..472616297 --- /dev/null +++ b/libs/prisma-service/prisma/migrations/20231007125030_endorse_transaction_type_date/migration.sql @@ -0,0 +1,7 @@ +-- AlterTable +ALTER TABLE "endorsement_transaction" ADD COLUMN "createDateTime" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, +ADD COLUMN "createdBy" INTEGER NOT NULL DEFAULT 1, +ADD COLUMN "deletedAt" TIMESTAMP(6), +ADD COLUMN "lastChangedBy" INTEGER NOT NULL DEFAULT 1, +ADD COLUMN "lastChangedDateTime" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, +ADD COLUMN "type" TEXT; diff --git a/libs/prisma-service/prisma/migrations/20231010055923_endorsement_transaction_schema_body/migration.sql b/libs/prisma-service/prisma/migrations/20231010055923_endorsement_transaction_schema_body/migration.sql new file mode 100644 index 000000000..77f492116 --- /dev/null +++ b/libs/prisma-service/prisma/migrations/20231010055923_endorsement_transaction_schema_body/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "endorsement_transaction" ADD COLUMN "requestBody" JSONB; diff --git a/libs/prisma-service/prisma/migrations/20231010065246_ecosystem_config/migration.sql b/libs/prisma-service/prisma/migrations/20231010065246_ecosystem_config/migration.sql new file mode 100644 index 000000000..0882ed891 --- /dev/null +++ b/libs/prisma-service/prisma/migrations/20231010065246_ecosystem_config/migration.sql @@ -0,0 +1,21 @@ +/* + Warnings: + - You are about to drop the column `enableEcosystem` on the `platform_config` table. All the data in the column will be lost. +*/ +-- AlterTable +ALTER TABLE "platform_config" DROP COLUMN "enableEcosystem"; + +-- CreateTable +CREATE TABLE "ecosystem_config" ( + "id" TEXT NOT NULL, + "url" TEXT NOT NULL, + "enableEcosystem" BOOLEAN NOT NULL DEFAULT false, + "autoEndorsement" BOOLEAN NOT NULL DEFAULT false, + "createDateTime" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "createdBy" INTEGER NOT NULL DEFAULT 1, + "lastChangedDateTime" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "lastChangedBy" INTEGER NOT NULL DEFAULT 1, + "deletedAt" TIMESTAMP(6), + + CONSTRAINT "ecosystem_config_pkey" PRIMARY KEY ("id") +); \ No newline at end of file diff --git a/libs/prisma-service/prisma/migrations/20231011095332_ecosystem_orgs_did/migration.sql b/libs/prisma-service/prisma/migrations/20231011095332_ecosystem_orgs_did/migration.sql new file mode 100644 index 000000000..00b7d3514 --- /dev/null +++ b/libs/prisma-service/prisma/migrations/20231011095332_ecosystem_orgs_did/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE "ecosystem_orgs" ADD COLUMN "orgDid" VARCHAR, +ADD COLUMN "orgName" TEXT; diff --git a/libs/prisma-service/prisma/migrations/20231012093420_ecosystem_config_key_value/migration.sql b/libs/prisma-service/prisma/migrations/20231012093420_ecosystem_config_key_value/migration.sql new file mode 100644 index 000000000..77cad7115 --- /dev/null +++ b/libs/prisma-service/prisma/migrations/20231012093420_ecosystem_config_key_value/migration.sql @@ -0,0 +1,25 @@ +/* + Warnings: + + - You are about to drop the column `autoEndorsement` on the `ecosystem_config` table. All the data in the column will be lost. + - You are about to drop the column `enableEcosystem` on the `ecosystem_config` table. All the data in the column will be lost. + - You are about to drop the column `url` on the `ecosystem_config` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "ecosystem_config" DROP COLUMN "autoEndorsement", +DROP COLUMN "enableEcosystem", +DROP COLUMN "url", +ADD COLUMN "key" TEXT, +ADD COLUMN "value" TEXT, +ALTER COLUMN "createdBy" SET DEFAULT '1', +ALTER COLUMN "createdBy" SET DATA TYPE TEXT, +ALTER COLUMN "lastChangedBy" SET DEFAULT '1', +ALTER COLUMN "lastChangedBy" SET DATA TYPE TEXT; + +-- AlterTable +ALTER TABLE "ecosystem_orgs" ADD COLUMN "deploymentMode" TEXT, +ALTER COLUMN "createdBy" SET DEFAULT '1', +ALTER COLUMN "createdBy" SET DATA TYPE TEXT, +ALTER COLUMN "lastChangedBy" SET DEFAULT '1', +ALTER COLUMN "lastChangedBy" SET DATA TYPE TEXT; diff --git a/libs/prisma-service/prisma/schema.prisma b/libs/prisma-service/prisma/schema.prisma index 82c551246..d3d57fe61 100644 --- a/libs/prisma-service/prisma/schema.prisma +++ b/libs/prisma-service/prisma/schema.prisma @@ -200,6 +200,7 @@ model ledgers { networkString String @db.VarChar registerDIDEndpoint String @db.VarChar registerDIDPayload Json? + indyNamespace String? @db.VarChar org_agents org_agents[] } @@ -307,3 +308,106 @@ model presentations { orgId Int organisation organisation @relation(fields: [orgId], references: [id]) } + +model ecosystem_roles { + id String @id @default(uuid()) + name String @unique + description String + 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) + ecosystemOrgs ecosystem_orgs[] +} + +model ecosystem { + id String @id @default(uuid()) + name String + description String + tags String + 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) + logoUrl String? + ecosystemInvitations ecosystem_invitations[] + ecosystemOrgs ecosystem_orgs[] + ecosystemUsers ecosystem_users[] +} + +model ecosystem_invitations { + id String @id @default(uuid()) + email String + status String + ecosystemId String + userId String + orgId String + 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) + ecosystem ecosystem @relation(fields: [ecosystemId], references: [id]) +} + +model ecosystem_users { + id String @id @default(uuid()) + userId String + ecosystemId String + 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) + ecosystem ecosystem @relation(fields: [ecosystemId], references: [id]) +} + +model ecosystem_orgs { + id String @id @default(uuid()) + orgId String + orgName String? + orgDid String? @db.VarChar + status String + deploymentMode String? + ecosystemId String + ecosystemRoleId String + createDateTime DateTime @default(now()) @db.Timestamptz(6) + createdBy String @default("1") + lastChangedDateTime DateTime @default(now()) @db.Timestamptz(6) + lastChangedBy String @default("1") + deletedAt DateTime? @db.Timestamp(6) + ecosystem ecosystem @relation(fields: [ecosystemId], references: [id]) + ecosystemRole ecosystem_roles @relation(fields: [ecosystemRoleId], references: [id]) + endorsementTransaction endorsement_transaction[] +} + +model endorsement_transaction { + id String @id @default(uuid()) + endorserDid String + authorDid String + requestPayload String + responsePayload String + type String? + 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) + status String + ecosystemOrgId String + requestBody Json? + ecosystemOrgs ecosystem_orgs @relation(fields: [ecosystemOrgId], references: [id]) +} + +model ecosystem_config { + id String @id @default(uuid()) + key String? + value String? + createDateTime DateTime @default(now()) @db.Timestamptz(6) + createdBy String @default("1") + lastChangedDateTime DateTime @default(now()) @db.Timestamptz(6) + lastChangedBy String @default("1") + deletedAt DateTime? @db.Timestamp(6) +} diff --git a/libs/prisma-service/prisma/seed.ts b/libs/prisma-service/prisma/seed.ts index 8b6b6fb51..c7a1f4b23 100644 --- a/libs/prisma-service/prisma/seed.ts +++ b/libs/prisma-service/prisma/seed.ts @@ -113,6 +113,32 @@ const createLedger = async (): Promise => { } }; +const createEcosystemRoles = async (): Promise => { + try { + const { ecosystemRoleData } = JSON.parse(configData); + const ecosystemRoles = await prisma.ecosystem_roles.createMany({ + data: ecosystemRoleData + }); + + logger.log(ecosystemRoles); + } catch (e) { + logger.error('An error occurred seeding ecosystemRoles:', e); + } +}; + +const createEcosystemConfig = async (): Promise => { + try { + const { ecosystemConfigData } = JSON.parse(configData); + const configDetails = await prisma.ecosystem_config.createMany({ + data: ecosystemConfigData + }); + + logger.log(configDetails); + } catch (e) { + logger.error('An error occurred seeding createEcosystemConfig:', e); + } +}; + async function main(): Promise { await createPlatformConfig(); @@ -123,6 +149,9 @@ async function main(): Promise { await createPlatformUserOrgRoles(); await createOrgAgentTypes(); await createLedger(); + await createEcosystemRoles(); + await createEcosystemConfig(); + } diff --git a/nest-cli.json b/nest-cli.json index 68dc6c90e..c4cae4482 100644 --- a/nest-cli.json +++ b/nest-cli.json @@ -232,6 +232,15 @@ "compilerOptions": { "tsConfigPath": "libs/supabase/tsconfig.lib.json" } + }, + "ecosystem": { + "type": "application", + "root": "apps/ecosystem", + "entryFile": "main", + "sourceRoot": "apps/ecosystem/src", + "compilerOptions": { + "tsConfigPath": "apps/ecosystem/tsconfig.app.json" + } } } } \ No newline at end of file