From 67a4e990eaf3014dd7fa34f42c654d36ce992e8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9rard=20Dethier?= Date: Tue, 4 Jun 2024 14:43:58 +0200 Subject: [PATCH 1/2] feat: refactor account recovery. logion-network/logion-internal#1135 --- README.md | 12 - resources/mail/README.md | 20 +- resources/mail/all-documented-vars.pug | 17 +- resources/mail/protection-accepted.pug | 30 - resources/mail/protection-cancelled.pug | 33 - resources/mail/protection-rejected.pug | 16 - resources/mail/protection-requested.pug | 33 - resources/mail/protection-resubmitted.pug | 33 - resources/mail/protection-updated.pug | 52 -- resources/mail/recovery-accepted.pug | 6 +- resources/mail/recovery-cancelled.pug | 4 +- resources/mail/recovery-rejected.pug | 6 +- resources/mail/recovery-requested.pug | 4 +- resources/mail/recovery-resubmitted.pug | 37 - resources/schemas.json | 82 +-- sample-data/accept.json | 3 - sample-data/check.json | 3 - sample-data/create.json | 22 - sample-data/document.json | 4 - .../fetch_pending_by_legal_officer.json | 4 - sample-data/fetch_pending_by_requester.json | 4 - sample-data/reject.json | 4 - src/logion/app.support.ts | 10 +- src/logion/container/app.container.ts | 20 +- ...ller.ts => account_recovery.controller.ts} | 232 +++---- src/logion/controllers/components.ts | 71 +- src/logion/controllers/recovery.controller.ts | 8 +- .../controllers/secret_recovery.controller.ts | 10 +- .../vaulttransferrequest.controller.ts | 63 +- .../1717410755574-AccountRecoveryRequest.ts | 18 + ...est.model.ts => account_recovery.model.ts} | 100 +-- .../accountrecoveryrequest.service.ts | 51 ++ ...accountrecoverysynchronization.service.ts} | 20 +- .../services/blockconsumption.service.ts | 6 +- src/logion/services/notification.service.ts | 7 - .../services/protectionrequest.service.ts | 51 -- src/logion/services/workload.service.ts | 8 +- ...spec.ts => account_recovery.model.spec.ts} | 85 +-- test/integration/model/account_recovery.sql | 14 + .../integration/model/protection_requests.sql | 14 - test/resources/mail/protection-accepted.pug | 3 - test/resources/mail/protection-requested.pug | 5 - test/resources/mail/recovery-accepted.pug | 3 + test/resources/mail/recovery-requested.pug | 5 + .../account_recovery.controller.spec.ts | 545 +++++++++++++++ .../protectionrequest.controller.spec.ts | 652 ------------------ .../controllers/recovery.controller.spec.ts | 49 +- .../vaulttransferrequest.controller.spec.ts | 31 +- ...spec.ts => account_recovery.model.spec.ts} | 72 +- ...countrecoverysynchronizer.service.spec.ts} | 32 +- .../services/blockconsumption.service.spec.ts | 20 +- test/unit/services/notification-test-data.ts | 9 +- .../services/notification.service.spec.ts | 20 +- test/unit/services/workload.service.spec.ts | 26 +- 54 files changed, 1048 insertions(+), 1641 deletions(-) delete mode 100644 resources/mail/protection-accepted.pug delete mode 100644 resources/mail/protection-cancelled.pug delete mode 100644 resources/mail/protection-rejected.pug delete mode 100644 resources/mail/protection-requested.pug delete mode 100644 resources/mail/protection-resubmitted.pug delete mode 100644 resources/mail/protection-updated.pug delete mode 100644 resources/mail/recovery-resubmitted.pug delete mode 100644 sample-data/accept.json delete mode 100644 sample-data/check.json delete mode 100644 sample-data/create.json delete mode 100644 sample-data/document.json delete mode 100644 sample-data/fetch_pending_by_legal_officer.json delete mode 100644 sample-data/fetch_pending_by_requester.json delete mode 100644 sample-data/reject.json rename src/logion/controllers/{protectionrequest.controller.ts => account_recovery.controller.ts} (52%) create mode 100644 src/logion/migration/1717410755574-AccountRecoveryRequest.ts rename src/logion/model/{protectionrequest.model.ts => account_recovery.model.ts} (69%) create mode 100644 src/logion/services/accountrecoveryrequest.service.ts rename src/logion/services/{protectionsynchronization.service.ts => accountrecoverysynchronization.service.ts} (63%) delete mode 100644 src/logion/services/protectionrequest.service.ts rename test/integration/model/{protectionrequest.model.spec.ts => account_recovery.model.spec.ts} (61%) create mode 100644 test/integration/model/account_recovery.sql delete mode 100644 test/integration/model/protection_requests.sql delete mode 100644 test/resources/mail/protection-accepted.pug delete mode 100644 test/resources/mail/protection-requested.pug create mode 100644 test/resources/mail/recovery-accepted.pug create mode 100644 test/resources/mail/recovery-requested.pug create mode 100644 test/unit/controllers/account_recovery.controller.spec.ts delete mode 100644 test/unit/controllers/protectionrequest.controller.spec.ts rename test/unit/model/{protectionrequest.model.spec.ts => account_recovery.model.spec.ts} (63%) rename test/unit/services/{protectionsynchronization.service.spec.ts => accountrecoverysynchronizer.service.spec.ts} (70%) diff --git a/README.md b/README.md index a3a39e36..b574c14c 100644 --- a/README.md +++ b/README.md @@ -24,18 +24,6 @@ This project features: ## Logion API -### Protection requests - -Request IDs are randomly generated upon creation. Please note down the request ID you get on creation in order to use -it as a path variable in the other queries. - -- Create: `curl -v http://127.0.0.1:8088/api/protection-request -H "Content-Type: application/json" -d @sample-data/create.json | jq` -- Fetch by requester: `curl -vX PUT http://127.0.0.1:8088/api/protection-request -H "Content-Type: application/json" -d @sample-data/fetch_pending_by_requester.json | jq` -- Fetch by legal officer: `curl -vX PUT http://127.0.0.1:8088/api/protection-request -H "Content-Type: application/json" -d @ sample-data/fetch_pending_by_legal_officer.json | jq` -- Reject: `curl -v http://127.0.0.1:8088/api/protection-request//reject -H "Content-Type: application/json" -d @sample-data/reject.json | jq` -- Accept: `curl -v http://127.0.0.1:8088/api/protection-request//accept -H "Content-Type: application/json" -d @sample-data/accept.json | jq` -- Check: `curl -v http://127.0.0.1:8088/api/protection-request//check-activation -H "Content-Type: application/json" -d @sample-data/check.json | jq` - ### Authentication Authentication process is described [here](doc/Authentication.md). diff --git a/resources/mail/README.md b/resources/mail/README.md index 08f7e67c..350caf4a 100644 --- a/resources/mail/README.md +++ b/resources/mail/README.md @@ -25,19 +25,19 @@ All possible variables are available (for copy/paste) in this template: [all-doc legalOfficer.postalAddress.city legalOfficer.postalAddress.country -In the context of a protection (or recovery), all the variables defined above may also be prefixed with +In the context of an account recovery, all the variables defined above may also be prefixed with `otherLegalOfficer` instead of `legalOfficer`. -### Protection and Recovery Request - protection.requesterAddress - protection.otherLegalOfficerAddress - protection.addressToRecover - protection.createdOn - protection.isRecovery +### Recovery Request + recovery.requesterAddress + recovery.otherLegalOfficerAddress + recovery.addressToRecover + recovery.createdOn + recovery.isRecovery - protection.decision.decisionOn - protection.decision.rejectReason - protection.decision.locId + recovery.decision.decisionOn + recovery.decision.rejectReason + recovery.decision.locId ### LOC loc.id diff --git a/resources/mail/all-documented-vars.pug b/resources/mail/all-documented-vars.pug index 57a67bb7..5c06c40b 100644 --- a/resources/mail/all-documented-vars.pug +++ b/resources/mail/all-documented-vars.pug @@ -34,16 +34,15 @@ | #{otherLegalOfficer.postalAddress.city}; | #{otherLegalOfficer.postalAddress.country}; | -| === Protection === -| #{protection.requesterAddress.address}; -| #{protection.otherLegalOfficerAddress}; -| #{protection.addressToRecover}; -| #{protection.createdOn}; -| #{protection.isRecovery}; +| === Account Recovery === +| #{recovery.requesterAddress.address}; +| #{recovery.otherLegalOfficerAddress}; +| #{recovery.addressToRecover}; +| #{recovery.createdOn}; | -| #{protection.decision.decisionOn}; -| #{protection.decision.rejectReason}; -| #{protection.decision.locId}; +| #{recovery.decision.decisionOn}; +| #{recovery.decision.rejectReason}; +| #{recovery.decision.locId}; | | === LOC === | #{loc.id}; diff --git a/resources/mail/protection-accepted.pug b/resources/mail/protection-accepted.pug deleted file mode 100644 index 38ca4b0d..00000000 --- a/resources/mail/protection-accepted.pug +++ /dev/null @@ -1,30 +0,0 @@ -| logion notification - Protection request approval -| Dear #{walletUser.firstName} #{walletUser.lastName}, -| -| You receive this message because you are requesting the protection of Legal Officers through the logion blockchain network. -| Please note, you can't answer this email. The logion network will never ask you to provide any kind of information or access to a web address through its email notifications. -| -|_______________________________________________________________________________ -| -|THE CONTENT OF THIS EMAIL IS IMPORTANT -|*************************************** -| -|YOU HAVE TO KEEP A COPY IN A SAFE PLACE AS IT CONTAINS MANDATORY INFORMATION -|REQUESTED IN CASE OF A RECOVERY REQUEST: -|- LOGION OFFICER DETAILS -|- AND PROTECTED ACCOUNT NUMBER -| -|________________________________________________________________________________ -| -| One of your Legal Officers, #{legalOfficer.userIdentity.firstName} #{legalOfficer.userIdentity.lastName}, has accepted to protect your Polkadot account set as follows: -| -|#{protection.requesterAddress.address}. -| -|As a reminder, the Legal Officer in charge of your protection is the following one: -| -include /legal-officer-details.pug -| -| -| Upon approval of both selected Legal Officers, you will be able to activate your protection from your wallet in the "My Logion Protection" section. -| -include /footer.pug diff --git a/resources/mail/protection-cancelled.pug b/resources/mail/protection-cancelled.pug deleted file mode 100644 index 877ef4f9..00000000 --- a/resources/mail/protection-cancelled.pug +++ /dev/null @@ -1,33 +0,0 @@ -| logion notification - Protection request cancelled -| Dear #{legalOfficer.userIdentity.firstName} #{legalOfficer.userIdentity.lastName}, -| -| Please note, you can't answer this email. The logion network will never ask you to provide any kind of information or access to a web address through its email notifications. -| -| The following user has cancelled a protection request: -| __________________________________________________________ -| -| Name: -| ******* -| #{walletUser.firstName} #{walletUser.lastName} -| -| Identification key: -| ******************** -| #{protection.requesterAddress.address} -| -| Email: -| ******* -| #{walletUser.email} -| -| Telephone: -| ************ -| #{walletUser.phoneNumber} -| -| Address: -| ********* -| #{walletUserPostalAddress.line1} -| #{walletUserPostalAddress.line2} -| #{walletUserPostalAddress.postalCode} #{walletUserPostalAddress.city} -| #{walletUserPostalAddress.country} -| __________________________________________________________ -| -include /footer.pug diff --git a/resources/mail/protection-rejected.pug b/resources/mail/protection-rejected.pug deleted file mode 100644 index b65efb87..00000000 --- a/resources/mail/protection-rejected.pug +++ /dev/null @@ -1,16 +0,0 @@ -| logion notification - Protection request refusal -| Dear #{walletUser.firstName} #{walletUser.lastName}, -| -| You receive this message because you are requesting the protection of a Legal Officer through the logion blockchain network. -| -| Please note, you can't answer this email. The logion network will never ask you to provide any kind of information or access to a web address through its email notifications. -| -| One of your Legal Officers, #{legalOfficer.userIdentity.firstName} #{legalOfficer.userIdentity.lastName}, has refused to protect your Polkadot account set as follows: #{protection.requesterAddress.address}, for the following reason: -| #{protection.decision.rejectReason} -| -include /legal-officer-details.pug -| -| -| Please contact your Legal Officer if you want more details. -| -include /footer.pug diff --git a/resources/mail/protection-requested.pug b/resources/mail/protection-requested.pug deleted file mode 100644 index dcd868a7..00000000 --- a/resources/mail/protection-requested.pug +++ /dev/null @@ -1,33 +0,0 @@ -| logion notification - Protection request to be reviewed -| Dear #{legalOfficer.userIdentity.firstName} #{legalOfficer.userIdentity.lastName}, -| -| Please note, you can't answer this email. The logion network will never ask you to provide any kind of information or access to a web address through its email notifications. -| -| The following user has requested your protection. Please go to your logion application and start the review and approval process: -| __________________________________________________________ -| -| Name: -| ******* -| #{walletUser.firstName} #{walletUser.lastName} -| -| Identification key: -| ******************** -| #{protection.requesterAddress.address} -| -| Email: -| ******* -| #{walletUser.email} -| -| Telephone: -| ************ -| #{walletUser.phoneNumber} -| -| Address: -| ********* -| #{walletUserPostalAddress.line1} -| #{walletUserPostalAddress.line2} -| #{walletUserPostalAddress.postalCode} #{walletUserPostalAddress.city} -| #{walletUserPostalAddress.country} -| __________________________________________________________ -| -include /footer.pug diff --git a/resources/mail/protection-resubmitted.pug b/resources/mail/protection-resubmitted.pug deleted file mode 100644 index 42bf5105..00000000 --- a/resources/mail/protection-resubmitted.pug +++ /dev/null @@ -1,33 +0,0 @@ -| logion notification - Protection request re-submitted -| Dear #{legalOfficer.userIdentity.firstName} #{legalOfficer.userIdentity.lastName}, -| -| Please note, you can't answer this email. The logion network will never ask you to provide any kind of information or access to a web address through its email notifications. -| -| The following user has re-submitted a protection request. Please go to your logion application and start the review and approval process: -| __________________________________________________________ -| -| Name: -| ******* -| #{walletUser.firstName} #{walletUser.lastName} -| -| Identification key: -| ******************** -| #{protection.requesterAddress.address} -| -| Email: -| ******* -| #{walletUser.email} -| -| Telephone: -| ************ -| #{walletUser.phoneNumber} -| -| Address: -| ********* -| #{walletUserPostalAddress.line1} -| #{walletUserPostalAddress.line2} -| #{walletUserPostalAddress.postalCode} #{walletUserPostalAddress.city} -| #{walletUserPostalAddress.country} -| __________________________________________________________ -| -include /footer.pug diff --git a/resources/mail/protection-updated.pug b/resources/mail/protection-updated.pug deleted file mode 100644 index 8cc223ec..00000000 --- a/resources/mail/protection-updated.pug +++ /dev/null @@ -1,52 +0,0 @@ -| logion notification - Protection request updated -| Dear #{legalOfficer.userIdentity.firstName} #{legalOfficer.userIdentity.lastName}, -| -| Please note, you can't answer this email. The logion network will never ask you to provide any kind of information or access to a web address through its email notifications. -| -| The following user has chosen a new Legal Officer. -| __________________________________________________________ -| -| New Other Legal Officer: -| **************************** -| -| #{otherLegalOfficer.address}; -| #{otherLegalOfficer.additionalDetails}; -| #{otherLegalOfficer.node}; -| -| #{otherLegalOfficer.userIdentity.firstName}; -| #{otherLegalOfficer.userIdentity.lastName}; -| #{otherLegalOfficer.userIdentity.email}; -| #{otherLegalOfficer.userIdentity.phoneNumber}; -| -| #{otherLegalOfficer.postalAddress.company}; -| #{otherLegalOfficer.postalAddress.line1}; -| #{otherLegalOfficer.postalAddress.line2}; -| #{otherLegalOfficer.postalAddress.postalCode}; -| #{otherLegalOfficer.postalAddress.city}; -| #{otherLegalOfficer.postalAddress.country}; -| -| Name: -| ******* -| #{walletUser.firstName} #{walletUser.lastName} -| -| Identification key: -| ******************** -| #{protection.requesterAddress.address} -| -| Email: -| ******* -| #{walletUser.email} -| -| Telephone: -| ************ -| #{walletUser.phoneNumber} -| -| Address: -| ********* -| #{walletUserPostalAddress.line1} -| #{walletUserPostalAddress.line2} -| #{walletUserPostalAddress.postalCode} #{walletUserPostalAddress.city} -| #{walletUserPostalAddress.country} -| __________________________________________________________ -| -include /footer.pug diff --git a/resources/mail/recovery-accepted.pug b/resources/mail/recovery-accepted.pug index 56555a6c..0a5760e5 100644 --- a/resources/mail/recovery-accepted.pug +++ b/resources/mail/recovery-accepted.pug @@ -5,14 +5,14 @@ | | Please note, you can't answer this email. The logion network will never ask you to provide any kind of information or access to a web address through its email notifications. | -| One of your Legal Officers, #{legalOfficer.userIdentity.firstName} #{legalOfficer.userIdentity.lastName}, has accepted to let you recover all digital assets from your Polkadot account set as follows: #{protection.requesterAddress.address}. +| One of your Legal Officers, #{legalOfficer.userIdentity.firstName} #{legalOfficer.userIdentity.lastName}, has accepted to let you recover all digital assets from your Polkadot account set as follows: #{recovery.requesterAddress.address}. | | As a reminder, find below all details of the Legal Officer who accepted this request: | include /legal-officer-details.pug | | Upon acceptance of both selected Legal Officers, you will be able, from the "Recovery" section of your wallet, to: -| * activate the protection of your new account by the same Legal Officers -| * recover all digital assets from your previous account (#{protection.addressToRecover}) by transfering them to the new one +| * activate the recovery of your new account by the same Legal Officers +| * recover all digital assets from your previous account (#{recovery.addressToRecover}) by transfering them to the new one | include /footer.pug diff --git a/resources/mail/recovery-cancelled.pug b/resources/mail/recovery-cancelled.pug index bd165f3a..7f64597b 100644 --- a/resources/mail/recovery-cancelled.pug +++ b/resources/mail/recovery-cancelled.pug @@ -12,11 +12,11 @@ | | Identification key to recover: | *********************************** -| #{protection.addressToRecover} +| #{recovery.addressToRecover} | | New Identification key: | ************************** -| #{protection.requesterAddress.address} +| #{recovery.requesterAddress.address} | | Email: | ******* diff --git a/resources/mail/recovery-rejected.pug b/resources/mail/recovery-rejected.pug index 2e46cbb0..265c5eb0 100644 --- a/resources/mail/recovery-rejected.pug +++ b/resources/mail/recovery-rejected.pug @@ -1,11 +1,11 @@ | logion notification - Recovery request refusal | Dear #{walletUser.firstName} #{walletUser.lastName}, | -| You receive this message because you are requesting a recovery process from a Legal Officer through the logion blockchain network. +| You receive this message because you are requesting an account recovery from a Legal Officer through the logion blockchain network. | Please note, you can't answer this email. The logion network will never ask you to provide any kind of information or access to a web address through its email notifications. | -| One of your Legal Officers, #{legalOfficer.userIdentity.firstName} #{legalOfficer.userIdentity.lastName}, has refused to let you recover assets from the Polkadot account you mentioned in your request (#{protection.requesterAddress.address}) for the following reason: -| #{protection.decision.rejectReason} +| One of your Legal Officers, #{legalOfficer.userIdentity.firstName} #{legalOfficer.userIdentity.lastName}, has refused to let you recover assets from the Polkadot account you mentioned in your request (#{recovery.requesterAddress.address}) for the following reason: +| #{recovery.decision.rejectReason} | | As a reminder, find below all details of the Legal Officer who refused this request: | diff --git a/resources/mail/recovery-requested.pug b/resources/mail/recovery-requested.pug index c82cf748..f2e7f1d9 100644 --- a/resources/mail/recovery-requested.pug +++ b/resources/mail/recovery-requested.pug @@ -12,11 +12,11 @@ | | Identification key to recover: | *********************************** -| #{protection.addressToRecover} +| #{recovery.addressToRecover} | | New Identification key: | ************************** -| #{protection.requesterAddress.address} +| #{recovery.requesterAddress.address} | | Email: | ******* diff --git a/resources/mail/recovery-resubmitted.pug b/resources/mail/recovery-resubmitted.pug deleted file mode 100644 index 65df44cb..00000000 --- a/resources/mail/recovery-resubmitted.pug +++ /dev/null @@ -1,37 +0,0 @@ -| logion notification - Recovery request re-submitted -| Dear #{legalOfficer.userIdentity.firstName} #{legalOfficer.userIdentity.lastName}, -| -| Please note, you can't answer this email. The logion network will never ask you to provide any kind of information or access to a web address through its email notifications. -| -| The following user has re-submitted a recovery request, please go to your logion application and start the review and approval process: -| __________________________________________________________ -| -| Name: -| ******* -| #{walletUser.firstName} #{walletUser.lastName} -| -| Identification key to recover: -| *********************************** -| #{protection.addressToRecover} -| -| New Identification key: -| ************************** -| #{protection.requesterAddress.address} -| -| Email: -| ******* -| #{walletUser.email} -| -| Telephone: -| ************ -| #{walletUser.phoneNumber} -| -| Address: -| ********* -| #{walletUserPostalAddress.line1} -| #{walletUserPostalAddress.line2} -| #{walletUserPostalAddress.postalCode} #{walletUserPostalAddress.city} -| #{walletUserPostalAddress.country} -| __________________________________________________________ -| -include /footer.pug diff --git a/resources/schemas.json b/resources/schemas.json index 3b188613..d2ff6599 100644 --- a/resources/schemas.json +++ b/resources/schemas.json @@ -169,16 +169,12 @@ } } }, - "CreateProtectionRequestView": { + "CreateAccountRecoveryRequestView": { "type": "object", "properties": { "addressToRecover": { "type": "string", - "description": "If this request is a recovery request, tells the address to recover" - }, - "isRecovery": { - "type": "boolean", - "description": "True if the protection request is also a recovery request" + "description": "Tells the address to recover" }, "legalOfficerAddress": { "type": "string", @@ -195,45 +191,34 @@ } }, "required": [ - "isRecovery", + "addressToRecover", "legalOfficerAddress", "otherLegalOfficerAddress", "requesterIdentityLoc" ], - "title": "CreateProtectionRequestView", - "description": "A Protection Request to create" - }, - "UpdateProtectionRequestView": { - "type": "object", - "properties": { - "otherLegalOfficerAddress": { - "type": "string", - "description": "The SS58 address of the other legal officer a new request is submitted to" - } - }, - "title": "AcceptProtectionRequestView", - "description": "Parameters for Protection Request's acceptance" + "title": "CreateAccountRecoveryRequestView", + "description": "An Account Recovery Request to create" }, - "FetchProtectionRequestsResponseView": { + "FetchAccountRecoveryRequestsResponseView": { "type": "object", "properties": { "requests": { "type": "array", - "description": "The Protection Requests matching provided specification", + "description": "The Account Recovery Requests matching provided specification", "items": { - "$ref": "#/components/schemas/ProtectionRequestView" + "$ref": "#/components/schemas/AccountRecoveryRequestView" } } }, - "title": "FetchProtectionRequestsResponseView", - "description": "The fetched Protection Requests" + "title": "FetchAccountRecoveryRequestsResponseView", + "description": "The fetched Account Recovery Requests" }, - "FetchProtectionRequestsSpecificationView": { + "FetchAccountRecoveryRequestsSpecificationView": { "type": "object", "properties": { "statuses": { "type": "array", - "description": "The statuses of expected Protection Requests", + "description": "The statuses of expected Account Recovery Requests", "uniqueItems": true, "items": { "type": "string", @@ -248,26 +233,17 @@ ] } }, - "kind": { - "type": "string", - "description": "The kind of protection request to be returned", - "enum": [ - "ANY", - "PROTECTION_ONLY", - "RECOVERY" - ] - }, "requesterAddress": { "type": "string", - "description": "The SS58 address of the requester in expected Protection Requests" + "description": "The SS58 address of the requester in expected Account Recovery Requests" }, "legalOfficerAddress": { "type": "string", - "description": "The SS58 address of the legal officer in expected Protection Requests" + "description": "The SS58 address of the legal officer in expected Account Recovery Requests" } }, - "title": "FetchProtectionRequestsSpecificationView", - "description": "The specification for fetching Protection Requests" + "title": "FetchAccountRecoveryRequestsSpecificationView", + "description": "The specification for fetching Account Recovery Requests" }, "FetchTransactionsResponseView": { "type": "object", @@ -346,7 +322,7 @@ "title": "PostalAddressView", "description": "A postal address" }, - "ProtectionRequestView": { + "AccountRecoveryRequestView": { "type": "object", "properties": { "addressToRecover": { @@ -364,11 +340,7 @@ "id": { "type": "string", "format": "uuid", - "description": "The ID of created Protection Request" - }, - "isRecovery": { - "type": "boolean", - "description": "True if the protection request is also a recovery request" + "description": "The ID of created Account Recovery Request" }, "legalOfficerAddress": { "type": "string", @@ -409,8 +381,8 @@ "$ref": "#/components/schemas/PostalAddressView" } }, - "title": "ProtectionRequestView", - "description": "Information about the created Protection Request" + "title": "AccountRecoveryRequestView", + "description": "Information about the created Account Recovery Request" }, "RecoveryInfoIdentityView": { "type": "object", @@ -1530,21 +1502,21 @@ } }, "title": "CreateVaultTransferRequestView", - "description": "A Protection Request to create" + "description": "A Account Recovery Request to create" }, "FetchVaultTransferRequestsResponseView": { "type": "object", "properties": { "requests": { "type": "array", - "description": "The Protection Requests matching provided specification", + "description": "The Account Recovery Requests matching provided specification", "items": { "$ref": "#/components/schemas/VaultTransferRequestView" } } }, "title": "FetchVaultTransferRequestsResponseView", - "description": "The fetched Protection Requests" + "description": "The fetched Account Recovery Requests" }, "VaultTransferRequestStatusView": { "title": "VaultTransferRequestStatusView", @@ -1610,7 +1582,7 @@ "id": { "type": "string", "format": "uuid", - "description": "The ID of created Protection Request" + "description": "The ID of created Account Recovery Request" }, "origin": { "type": "string", @@ -1643,7 +1615,7 @@ } }, "title": "VaultTransferRequestView", - "description": "Information about the created Protection Request" + "description": "Information about the created Account Recovery Request" }, "RejectVaultTransferRequestView": { "type": "object", @@ -1654,7 +1626,7 @@ } }, "title": "RejectVaultTransferRequestView", - "description": "The Protection Request to reject" + "description": "The Account Recovery Request to reject" }, "CreateSofRequestView": { "type": "object", @@ -2024,7 +1996,7 @@ "id": { "type": "string", "format": "uuid", - "description": "The ID of created Protection Request" + "description": "The ID of created Account Recovery Request" }, "rejectReason": { "type": "string", diff --git a/sample-data/accept.json b/sample-data/accept.json deleted file mode 100644 index d277a163..00000000 --- a/sample-data/accept.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "legalOfficerAddress": "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY" -} \ No newline at end of file diff --git a/sample-data/check.json b/sample-data/check.json deleted file mode 100644 index 3b88435e..00000000 --- a/sample-data/check.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "userAddress": "5Ew3MyB15VprZrjQVkpQFj8okmc9xLDSEdNhqMMS5cXsqxoW" -} \ No newline at end of file diff --git a/sample-data/create.json b/sample-data/create.json deleted file mode 100644 index 3a95fe3e..00000000 --- a/sample-data/create.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "requesterAddress": "5Ew3MyB15VprZrjQVkpQFj8okmc9xLDSEdNhqMMS5cXsqxoW", - "userIdentity": { - "firstName": "Gérard", - "lastName": "Dethier", - "email": "gerard@logion.network", - "phoneNumber": "+123456" - }, - "userPostalAddress": { - "line1": "L", - "line2": "L", - "postalCode": "P", - "city": "C", - "country": "C" - }, - "legalOfficerAddresses": [ - "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty", - "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY" - ], - "isRecovery": false, - "addressToRecover": null -} \ No newline at end of file diff --git a/sample-data/document.json b/sample-data/document.json deleted file mode 100644 index 3a70bda8..00000000 --- a/sample-data/document.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "_id": "test-id", - "value": "test-value" -} \ No newline at end of file diff --git a/sample-data/fetch_pending_by_legal_officer.json b/sample-data/fetch_pending_by_legal_officer.json deleted file mode 100644 index 04601b83..00000000 --- a/sample-data/fetch_pending_by_legal_officer.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "legalOfficerAddress":"5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty", - "protectionRequestStatus":"PENDING" -} \ No newline at end of file diff --git a/sample-data/fetch_pending_by_requester.json b/sample-data/fetch_pending_by_requester.json deleted file mode 100644 index 5f8e17a1..00000000 --- a/sample-data/fetch_pending_by_requester.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "requesterAddress":"5Ew3MyB15VprZrjQVkpQFj8okmc9xLDSEdNhqMMS5cXsqxoW", - "protectionRequestStatus":"PENDING" -} \ No newline at end of file diff --git a/sample-data/reject.json b/sample-data/reject.json deleted file mode 100644 index 85a7fc9e..00000000 --- a/sample-data/reject.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "legalOfficerAddress": "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty", - "rejectReason": "-" -} \ No newline at end of file diff --git a/src/logion/app.support.ts b/src/logion/app.support.ts index 45bf61d7..7c59978b 100644 --- a/src/logion/app.support.ts +++ b/src/logion/app.support.ts @@ -1,9 +1,9 @@ import { OpenAPIV3 } from "openapi-types"; import expressOasGenerator, { SPEC_OUTPUT_FILE_BEHAVIOR } from 'express-oas-generator'; import { - fillInSpec as fillInSpecForProtectionController, - ProtectionRequestController -} from "./controllers/protectionrequest.controller.js"; + fillInSpec as fillInSpecForAccountRecoveryController, + AccountRecoveryController +} from "./controllers/account_recovery.controller.js"; import { fillInSpec as fillInSpecForTransaction, TransactionController } from "./controllers/transaction.controller.js"; import { configureOpenApi, configureDinoloop, setOpenApi3, loadSchemasIntoSpec, Log } from "@logion/rest-api-core"; import { fillInSpec as fillInSpecForLoc, LocRequestController } from "./controllers/locrequest.controller.js"; @@ -51,7 +51,7 @@ export function predefinedSpec(spec: OpenAPIV3.Document): OpenAPIV3.Document { version: "0.1", }; - fillInSpecForProtectionController(spec); + fillInSpecForAccountRecoveryController(spec); fillInSpecForTransaction(spec); fillInSpecForLoc(spec); fillInSpecForCollection(spec); @@ -115,7 +115,7 @@ export function setupApp(expressConfig?: ExpressConfig): Express { dino.useRouter(() => express.Router()); configureDinoloop(dino); - dino.registerController(ProtectionRequestController); + dino.registerController(AccountRecoveryController); dino.registerController(TransactionController); dino.registerController(LocRequestController); dino.registerController(CollectionController); diff --git a/src/logion/container/app.container.ts b/src/logion/container/app.container.ts index ef118e3d..3dc45b5e 100644 --- a/src/logion/container/app.container.ts +++ b/src/logion/container/app.container.ts @@ -1,8 +1,8 @@ import { configureContainer, HealthService } from "@logion/rest-api-core"; import { Container } from 'inversify'; -import { ProtectionRequestController } from '../controllers/protectionrequest.controller.js'; -import { ProtectionRequestRepository, ProtectionRequestFactory } from '../model/protectionrequest.model.js'; +import { AccountRecoveryController } from '../controllers/account_recovery.controller.js'; +import { AccountRecoveryRepository, AccountRecoveryRequestFactory } from '../model/account_recovery.model.js'; import { TransactionRepository, TransactionFactory } from '../model/transaction.model.js'; import { SyncPointRepository, SyncPointFactory } from '../model/syncpoint.model.js'; import { BlockExtrinsicsService } from "../services/block.service.js"; @@ -15,7 +15,7 @@ import { Scheduler } from '../scheduler/scheduler.service.js'; import { LocRequestController } from "../controllers/locrequest.controller.js"; import { LocRequestRepository, LocRequestFactory } from "../model/locrequest.model.js"; import { FileStorageService } from '../services/file.storage.service.js'; -import { ProtectionSynchronizer } from '../services/protectionsynchronization.service.js'; +import { AccountRecoverySynchronizer } from '../services/accountrecoverysynchronization.service.js'; import { TransactionController } from '../controllers/transaction.controller.js'; import { CollectionRepository, CollectionFactory } from "../model/collection.model.js"; import { NotificationService } from "../services/notification.service.js"; @@ -40,7 +40,7 @@ import { VerifiedIssuerSelectionFactory, VerifiedIssuerSelectionRepository } fro import { LocRequestAdapter } from "../controllers/adapters/locrequestadapter.js"; import { LocRequestService, TransactionalLocRequestService } from "../services/locrequest.service.js"; import { LoFileService, TransactionalLoFileService } from "../services/lofile.service.js"; -import { ProtectionRequestService, TransactionalProtectionRequestService } from "../services/protectionrequest.service.js"; +import { AccountRecoveryRequestService, TransactionalAccountRecoveryRequestService } from "../services/accountrecoveryrequest.service.js"; import { SettingService, TransactionalSettingService } from "../services/settings.service.js"; import { SyncPointService, TransactionalSyncPointService } from "../services/syncpoint.service.js"; import { TransactionalTransactionService, TransactionService } from "../services/transaction.service.js"; @@ -74,8 +74,8 @@ import { RecoveryController } from "../controllers/recovery.controller.js"; const container = new Container({ defaultScope: "Singleton", skipBaseClassChecks: true }); configureContainer(container); -container.bind(ProtectionRequestRepository).toSelf(); -container.bind(ProtectionRequestFactory).toSelf(); +container.bind(AccountRecoveryRepository).toSelf(); +container.bind(AccountRecoveryRequestFactory).toSelf(); container.bind(BlockExtrinsicsService).toSelf(); container.bind(ExtrinsicDataExtractor).toSelf(); container.bind(TransactionExtractor).toSelf(); @@ -90,7 +90,7 @@ container.bind(LocRequestFactory).toSelf(); container.bind(FileStorageService).toSelf(); container.bind(LocSynchronizer).toSelf(); container.bind(BlockConsumer).toSelf(); -container.bind(ProtectionSynchronizer).toSelf(); +container.bind(AccountRecoverySynchronizer).toSelf(); container.bind(CollectionRepository).toSelf() container.bind(CollectionFactory).toSelf() container.bind(LogionNodeCollectionService).toSelf(); @@ -123,8 +123,8 @@ container.bind(CollectionService).toService(TransactionalCollectionService); container.bind(TransactionalCollectionService).toSelf(); container.bind(LoFileService).toService(TransactionalLoFileService); container.bind(TransactionalLoFileService).toSelf(); -container.bind(ProtectionRequestService).toService(TransactionalProtectionRequestService); -container.bind(TransactionalProtectionRequestService).toSelf(); +container.bind(AccountRecoveryRequestService).toService(TransactionalAccountRecoveryRequestService); +container.bind(TransactionalAccountRecoveryRequestService).toSelf(); container.bind(SettingService).toService(TransactionalSettingService); container.bind(TransactionalSettingService).toSelf(); container.bind(SyncPointService).toService(TransactionalSyncPointService); @@ -165,7 +165,7 @@ container.bind(TransactionalSecretRecoveryRequestService).toSelf(); // Controllers are stateful so they must not be injected with singleton scope container.bind(LocRequestController).toSelf().inTransientScope(); -container.bind(ProtectionRequestController).toSelf().inTransientScope(); +container.bind(AccountRecoveryController).toSelf().inTransientScope(); container.bind(TransactionController).toSelf().inTransientScope(); container.bind(VaultTransferRequestController).toSelf().inTransientScope(); container.bind(SettingController).toSelf().inTransientScope(); diff --git a/src/logion/controllers/protectionrequest.controller.ts b/src/logion/controllers/account_recovery.controller.ts similarity index 52% rename from src/logion/controllers/protectionrequest.controller.ts rename to src/logion/controllers/account_recovery.controller.ts index 19c86334..3ad18f53 100644 --- a/src/logion/controllers/protectionrequest.controller.ts +++ b/src/logion/controllers/account_recovery.controller.ts @@ -17,52 +17,49 @@ import { } from '@logion/rest-api-core'; import { - ProtectionRequestRepository, - FetchProtectionRequestsSpecification, - ProtectionRequestFactory, - ProtectionRequestDescription, - ProtectionRequestStatus, -} from '../model/protectionrequest.model.js'; + AccountRecoveryRepository, + FetchAccountRecoveryRequestsSpecification, + AccountRecoveryRequestFactory, + AccountRecoveryRequestDescription, + AccountRecoveryRequestStatus, +} from '../model/account_recovery.model.js'; import { components } from './components.js'; import { NotificationService, Template, NotificationRecipient } from "../services/notification.service.js"; import { DirectoryService } from "../services/directory.service.js"; -import { ProtectionRequestService } from '../services/protectionrequest.service.js'; +import { AccountRecoveryRequestService } from '../services/accountrecoveryrequest.service.js'; import { LocalsObject } from 'pug'; import { LocRequestAdapter, UserPrivateData } from "./adapters/locrequestadapter.js"; import { LocRequestRepository } from '../model/locrequest.model.js'; import { ValidAccountId } from "@logion/node-api"; import { LegalOfficerDecisionDescription } from '../model/decision.js'; -type CreateProtectionRequestView = components["schemas"]["CreateProtectionRequestView"]; -type ProtectionRequestView = components["schemas"]["ProtectionRequestView"]; -type FetchProtectionRequestsSpecificationView = components["schemas"]["FetchProtectionRequestsSpecificationView"]; -type FetchProtectionRequestsResponseView = components["schemas"]["FetchProtectionRequestsResponseView"]; +type CreateAccountRecoveryRequestView = components["schemas"]["CreateAccountRecoveryRequestView"]; +type AccountRecoveryRequestView = components["schemas"]["AccountRecoveryRequestView"]; +type FetchAccountRecoveryRequestsSpecificationView = components["schemas"]["FetchAccountRecoveryRequestsSpecificationView"]; +type FetchAccountRecoveryRequestsResponseView = components["schemas"]["FetchAccountRecoveryRequestsResponseView"]; type RejectRecoveryRequestView = components["schemas"]["RejectRecoveryRequestView"]; -type UpdateProtectionRequestView = components["schemas"]["UpdateProtectionRequestView"]; type RecoveryInfoView = components["schemas"]["RecoveryInfoView"]; type RecoveryInfoIdentityView = components["schemas"]["RecoveryInfoIdentityView"]; const { logger } = Log; export function fillInSpec(spec: OpenAPIV3.Document): void { - const tagName = 'Protection Requests'; + const tagName = 'Account Recovery Requests'; addTag(spec, { name: tagName, - description: "Handling of Protection Requests" + description: "Handling of Account Recovery Requests" }); - setControllerTag(spec, /^\/api\/protection-request.*/, tagName); - - ProtectionRequestController.createProtectionRequest(spec); - ProtectionRequestController.fetchProtectionRequests(spec); - ProtectionRequestController.rejectProtectionRequest(spec); - ProtectionRequestController.acceptProtectionRequest(spec); - ProtectionRequestController.fetchRecoveryInfo(spec); - ProtectionRequestController.resubmit(spec); - ProtectionRequestController.cancel(spec); - ProtectionRequestController.update(spec); + setControllerTag(spec, /^\/api\/account-recovery.*/, tagName); + + AccountRecoveryController.createRequest(spec); + AccountRecoveryController.fetchRequests(spec); + AccountRecoveryController.rejectRequest(spec); + AccountRecoveryController.acceptRequest(spec); + AccountRecoveryController.fetchRecoveryInfo(spec); + AccountRecoveryController.cancel(spec); } -interface ProtectionRequestPublicFields { +interface AccountRecoveryRequestPublicFields { id?: string; @@ -70,13 +67,11 @@ interface ProtectionRequestPublicFields { createdOn?: string; - isRecovery?: boolean; - getRequester(): ValidAccountId; requesterIdentityLocId?: string; - status?: ProtectionRequestStatus; + status?: AccountRecoveryRequestStatus; decision?: { decisionOn?: string, rejectReason?: string }; @@ -86,82 +81,79 @@ interface ProtectionRequestPublicFields { } @injectable() -@Controller('/protection-request') -export class ProtectionRequestController extends ApiController { +@Controller('/account-recovery') +export class AccountRecoveryController extends ApiController { constructor( - private protectionRequestRepository: ProtectionRequestRepository, - private protectionRequestFactory: ProtectionRequestFactory, + private accountRecoveryRequestRepository: AccountRecoveryRepository, + private accountRecoveryRequestFactory: AccountRecoveryRequestFactory, private authenticationService: AuthenticationService, private notificationService: NotificationService, private directoryService: DirectoryService, - private protectionRequestService: ProtectionRequestService, + private accountRecoveryRequestService: AccountRecoveryRequestService, private locRequestAdapter: LocRequestAdapter, private locRequestRepository: LocRequestRepository, ) { super(); } - static createProtectionRequest(spec: OpenAPIV3.Document) { - const operationObject = spec.paths["/api/protection-request"].post!; - operationObject.summary = "Creates a new Protection Request"; - operationObject.description = "The authenticated user must be the protection/recovery requester"; + static createRequest(spec: OpenAPIV3.Document) { + const operationObject = spec.paths["/api/account-recovery"].post!; + operationObject.summary = "Creates a new Account Recovery Request"; + operationObject.description = "The authenticated user must be the recovery requester"; operationObject.requestBody = getRequestBody({ - description: "Protection request creation data", - view: "CreateProtectionRequestView", + description: "Account Recovery request creation data", + view: "CreateAccountRecoveryRequestView", }); - operationObject.responses = getDefaultResponses("ProtectionRequestView"); + operationObject.responses = getDefaultResponses("AccountRecoveryRequestView"); } @Async() @HttpPost('') - async createProtectionRequest(body: CreateProtectionRequestView): Promise { + async createRequest(body: CreateAccountRecoveryRequestView): Promise { const requester = await this.authenticationService.authenticatedUser(this.request); const legalOfficerAddress = await this.directoryService.requireLegalOfficerAddressOnNode(body.legalOfficerAddress); const requesterIdentityLoc = requireDefined(body.requesterIdentityLoc); - const request = await this.protectionRequestFactory.newProtectionRequest({ + const request = await this.accountRecoveryRequestFactory.newAccountRecoveryRequest({ id: uuid(), requesterAddress: requester.validAccountId, requesterIdentityLoc, legalOfficerAddress, otherLegalOfficerAddress: ValidAccountId.polkadot(body.otherLegalOfficerAddress), createdOn: moment().toISOString(), - isRecovery: body.isRecovery, - addressToRecover: body.addressToRecover ? ValidAccountId.polkadot(body.addressToRecover) : null, + addressToRecover: ValidAccountId.polkadot(body.addressToRecover), }); - await this.protectionRequestService.add(request); - const templateId: Template = request.isRecovery ? "recovery-requested" : "protection-requested" + await this.accountRecoveryRequestService.add(request); const userPrivateData = await this.locRequestAdapter.getUserPrivateData(requesterIdentityLoc) - this.notify("LegalOfficer", templateId, request.getDescription(), userPrivateData) + this.notify("LegalOfficer", "recovery-requested", request.getDescription(), userPrivateData) return this.adapt(request, userPrivateData); } - static fetchProtectionRequests(spec: OpenAPIV3.Document) { - const operationObject = spec.paths["/api/protection-request"].put!; - operationObject.summary = "Lists Protection Requests based on a given specification"; - operationObject.description = "The authenticated user must be either the requester or one of the legal officers of the expected protection requests."; + static fetchRequests(spec: OpenAPIV3.Document) { + const operationObject = spec.paths["/api/account-recovery"].put!; + operationObject.summary = "Lists requests based on a given specification"; + operationObject.description = "The authenticated user must be either the requester or one of the legal officers of the expected requests."; operationObject.requestBody = getRequestBody({ - description: "The specification for fetching Protection Requests", - view: "FetchProtectionRequestsSpecificationView", + description: "The specification for fetching requests", + view: "FetchAccountRecoveryRequestsSpecificationView", }); - operationObject.responses = getDefaultResponses("FetchProtectionRequestsResponseView"); + operationObject.responses = getDefaultResponses("FetchAccountRecoveryRequestsResponseView"); } @Async() @HttpPut('') - async fetchProtectionRequests(body: FetchProtectionRequestsSpecificationView): Promise { + async fetchRequests(body: FetchAccountRecoveryRequestsSpecificationView): Promise { const authenticatedUser = await this.authenticationService.authenticatedUser(this.request); const requester = body.requesterAddress ? ValidAccountId.polkadot(body.requesterAddress) : undefined; const legalOfficer = body.legalOfficerAddress ? ValidAccountId.polkadot(body.legalOfficerAddress) : undefined; authenticatedUser.require(user => user.isOneOf([ legalOfficer, requester ])); - const specification = new FetchProtectionRequestsSpecification({ + const specification = new FetchAccountRecoveryRequestsSpecification({ expectedRequesterAddress: requester, expectedLegalOfficerAddress: legalOfficer ? [ legalOfficer ] : undefined, expectedStatuses: body.statuses, - kind: body.kind, }); - const protectionRequests = await this.protectionRequestRepository.findBy(specification); - const requests = protectionRequests.map(request => + const accountRecoveryRequests = await this.accountRecoveryRequestRepository.findBy(specification); + const requests = accountRecoveryRequests.map(request => this.locRequestAdapter.getUserPrivateData(request.getDescription().requesterIdentityLocId) .then(userPrivateData => this.adapt(request, userPrivateData)) ); @@ -170,7 +162,7 @@ export class ProtectionRequestController extends ApiController { }; } - adapt(request: ProtectionRequestPublicFields, userPrivateData: UserPrivateData): ProtectionRequestView { + adapt(request: AccountRecoveryRequestPublicFields, userPrivateData: UserPrivateData): AccountRecoveryRequestView { const { userIdentity, userPostalAddress } = userPrivateData; return { id: request.id!, @@ -185,66 +177,59 @@ export class ProtectionRequestController extends ApiController { decisionOn: request.decision!.decisionOn || undefined, }, createdOn: request.createdOn!, - isRecovery: request.isRecovery || false, addressToRecover: request.getAddressToRecover()?.address, status: request.status!, }; } - static rejectProtectionRequest(spec: OpenAPIV3.Document) { - const operationObject = spec.paths["/api/protection-request/{id}/reject"].post!; - operationObject.summary = "Rejects a Protection Request"; - operationObject.description = "The authenticated user must be one of the legal officers of the protection request"; + static rejectRequest(spec: OpenAPIV3.Document) { + const operationObject = spec.paths["/api/account-recovery/{id}/reject"].post!; + operationObject.summary = "Rejects a request"; + operationObject.description = "The authenticated user must be one of the legal officers of the request"; operationObject.requestBody = getRequestBody({ - description: "Protection Request rejection data", + description: "Request rejection data", view: "RejectRecoveryRequestView", }); - operationObject.responses = getDefaultResponses("ProtectionRequestView"); + operationObject.responses = getDefaultResponses("AccountRecoveryRequestView"); setPathParameters(operationObject, { 'id': "The ID of the request to reject" }); } @Async() @HttpPost('/:id/reject') - async rejectProtectionRequest(body: RejectRecoveryRequestView, id: string): Promise { + async rejectRequest(body: RejectRecoveryRequestView, id: string): Promise { const authenticatedUser = await this.authenticationService.authenticatedUserIsLegalOfficerOnNode(this.request); - const request = await this.protectionRequestService.update(id, async request => { + const request = await this.accountRecoveryRequestService.update(id, async request => { authenticatedUser.require(user => user.is(request.getLegalOfficer())) request.reject(body.rejectReason!, moment()); }); - const templateId: Template = request.isRecovery ? "recovery-rejected" : "protection-rejected"; const userPrivateData = await this.locRequestAdapter.getUserPrivateData(request.requesterIdentityLocId!) - this.notify("WalletUser", templateId, request.getDescription(), userPrivateData, request.getDecision()); + this.notify("WalletUser", "recovery-rejected", request.getDescription(), userPrivateData, request.getDecision()); return this.adapt(request, userPrivateData); } - static acceptProtectionRequest(spec: OpenAPIV3.Document) { - const operationObject = spec.paths["/api/protection-request/{id}/accept"].post!; - operationObject.summary = "Accepts a Protection Request"; - operationObject.description = "The authenticated user must be one of the legal officers of the protection request"; - operationObject.requestBody = getRequestBody({ - description: "Protection Request acceptance data", - view: "AcceptProtectionRequestView", - }); - operationObject.responses = getDefaultResponses("ProtectionRequestView"); + static acceptRequest(spec: OpenAPIV3.Document) { + const operationObject = spec.paths["/api/account-recovery/{id}/accept"].post!; + operationObject.summary = "Accepts a request"; + operationObject.description = "The authenticated user must be one of the legal officers of the request"; + operationObject.responses = getDefaultResponses("AccountRecoveryRequestView"); setPathParameters(operationObject, { 'id': "The ID of the request to accept" }); } @Async() @HttpPost('/:id/accept') - async acceptProtectionRequest(_body: never, id: string): Promise { + async acceptRequest(_body: never, id: string): Promise { const authenticatedUser = await this.authenticationService.authenticatedUserIsLegalOfficerOnNode(this.request); - const request = await this.protectionRequestService.update(id, async request => { + const request = await this.accountRecoveryRequestService.update(id, async request => { authenticatedUser.require(user => user.is(request.getLegalOfficer())) request.accept(moment()); }); - const templateId: Template = request.isRecovery ? "recovery-accepted" : "protection-accepted"; const userPrivateData = await this.locRequestAdapter.getUserPrivateData(request.requesterIdentityLocId!) - this.notify("WalletUser", templateId, request.getDescription(), userPrivateData, request.getDecision()); + this.notify("WalletUser", "recovery-accepted", request.getDescription(), userPrivateData, request.getDecision()); return this.adapt(request, userPrivateData); } static fetchRecoveryInfo(spec: OpenAPIV3.Document) { - const operationObject = spec.paths["/api/protection-request/{id}/recovery-info"].put!; + const operationObject = spec.paths["/api/account-recovery/{id}/recovery-info"].put!; operationObject.summary = "Fetch all info necessary for the legal officer to accept or reject account recovery request."; operationObject.description = "The authentication user must be a legal officers on current node"; operationObject.responses = getDefaultResponses("RecoveryInfoView"); @@ -256,11 +241,9 @@ export class ProtectionRequestController extends ApiController { async fetchRecoveryInfo(_body: never, id: string): Promise { const authenticatedUser = await this.authenticationService.authenticatedUserIsLegalOfficerOnNode(this.request); - const accountRecoveryRequest = await this.protectionRequestRepository.findById(id); + const accountRecoveryRequest = await this.accountRecoveryRequestRepository.findById(id); if(accountRecoveryRequest === null - || !accountRecoveryRequest.isRecovery - || accountRecoveryRequest.status !== 'PENDING' - || !accountRecoveryRequest.addressToRecover) { + || accountRecoveryRequest.status !== 'PENDING') { throw badRequest("Pending recovery request with address to recover not found"); } authenticatedUser.require(user => user.is(accountRecoveryRequest.getLegalOfficer())); @@ -293,31 +276,10 @@ export class ProtectionRequestController extends ApiController { }; } - static resubmit(spec: OpenAPIV3.Document) { - const operationObject = spec.paths["/api/protection-request/{id}/resubmit"].post!; - operationObject.summary = "Re-submit a Protection Request"; - operationObject.description = "The authenticated user must be the protection requester"; - operationObject.responses = getDefaultResponsesNoContent(); - setPathParameters(operationObject, { 'id': "The ID of the request to resubmit" }); - } - - @Async() - @HttpPost('/:id/resubmit') - @SendsResponse() - async resubmit(_body: never, id: string): Promise { - const authenticatedUser = await this.authenticationService.authenticatedUser(this.request); - const request = await this.protectionRequestService.update(id, async request => { - authenticatedUser.require(user => user.is(request.getRequester())); - request.resubmit(); - }); - this.notify("LegalOfficer", request.isRecovery ? 'recovery-resubmitted' : 'protection-resubmitted', request.getDescription()); - this.response.sendStatus(204); - } - static cancel(spec: OpenAPIV3.Document) { - const operationObject = spec.paths["/api/protection-request/{id}/cancel"].post!; - operationObject.summary = "Cancels a Protection Request"; - operationObject.description = "The authenticated user must be the protection requester"; + const operationObject = spec.paths["/api/account-recovery/{id}/cancel"].post!; + operationObject.summary = "Cancels a request"; + operationObject.description = "The authenticated user must be the requester"; operationObject.responses = getDefaultResponsesNoContent(); setPathParameters(operationObject, { 'id': "The ID of the request to cancel" }); } @@ -327,42 +289,16 @@ export class ProtectionRequestController extends ApiController { @SendsResponse() async cancel(_body: never, id: string): Promise { const authenticatedUser = await this.authenticationService.authenticatedUser(this.request); - const request = await this.protectionRequestService.update(id, async request => { + const request = await this.accountRecoveryRequestService.update(id, async request => { authenticatedUser.require(user => user.is(request.getRequester())); request.cancel(); }); - this.notify("LegalOfficer", request.isRecovery ? 'recovery-cancelled' : 'protection-cancelled', request.getDescription()); - this.response.sendStatus(204); - } - - static update(spec: OpenAPIV3.Document) { - const operationObject = spec.paths["/api/protection-request/{id}/update"].put!; - operationObject.summary = "Updates a Protection Request"; - operationObject.description = "The authenticated user must be the protection requester"; - operationObject.responses = getDefaultResponsesNoContent(); - operationObject.requestBody = getRequestBody({ - description: "Protection Request update data", - view: "UpdateProtectionRequestView", - }); - setPathParameters(operationObject, { 'id': "The ID of the request to update" }); - } - - @Async() - @HttpPut('/:id/update') - @SendsResponse() - async update(updateProtectionRequestView: UpdateProtectionRequestView, id: string): Promise { - const authenticatedUser = await this.authenticationService.authenticatedUser(this.request); - const otherLegalOfficerAddress = ValidAccountId.polkadot(requireDefined(updateProtectionRequestView.otherLegalOfficerAddress)); - const request = await this.protectionRequestService.update(id, async request => { - authenticatedUser.require(user => user.is(request.getRequester())); - request.updateOtherLegalOfficer(otherLegalOfficerAddress); - }); - this.notify("LegalOfficer", 'protection-updated', request.getDescription()); + this.notify("LegalOfficer", 'recovery-cancelled', request.getDescription()); this.response.sendStatus(204); } - private notify(recipient: NotificationRecipient, templateId: Template, protection: ProtectionRequestDescription, userPrivateData?: UserPrivateData, decision?: LegalOfficerDecisionDescription): void { - this.getNotificationInfo(protection, userPrivateData, decision) + private notify(recipient: NotificationRecipient, templateId: Template, request: AccountRecoveryRequestDescription, userPrivateData?: UserPrivateData, decision?: LegalOfficerDecisionDescription): void { + this.getNotificationInfo(request, userPrivateData, decision) .then(info => { const to = recipient === "WalletUser" ? info.userEmail : info.legalOfficerEMail return this.notificationService.notify(to, templateId, info.data) @@ -373,17 +309,17 @@ export class ProtectionRequestController extends ApiController { ) } - private async getNotificationInfo(protection: ProtectionRequestDescription, userPrivateData?: UserPrivateData, decision?: LegalOfficerDecisionDescription): + private async getNotificationInfo(request: AccountRecoveryRequestDescription, userPrivateData?: UserPrivateData, decision?: LegalOfficerDecisionDescription): Promise<{ legalOfficerEMail: string, userEmail: string | undefined, data: LocalsObject }> { - const legalOfficer = await this.directoryService.get(protection.legalOfficerAddress) - const otherLegalOfficer = await this.directoryService.get(protection.otherLegalOfficerAddress) - const { userIdentity, userPostalAddress } = userPrivateData ? userPrivateData : await this.locRequestAdapter.getUserPrivateData(protection.requesterIdentityLocId) + const legalOfficer = await this.directoryService.get(request.legalOfficerAddress) + const otherLegalOfficer = await this.directoryService.get(request.otherLegalOfficerAddress) + const { userIdentity, userPostalAddress } = userPrivateData ? userPrivateData : await this.locRequestAdapter.getUserPrivateData(request.requesterIdentityLocId) return { legalOfficerEMail: legalOfficer.userIdentity.email, userEmail: userIdentity?.email, data: { - protection: { ...protection, decision }, + recovery: { ...request, decision }, legalOfficer, otherLegalOfficer, walletUser: userIdentity, diff --git a/src/logion/controllers/components.ts b/src/logion/controllers/components.ts index 5bd96ec1..6f3b158b 100644 --- a/src/logion/controllers/components.ts +++ b/src/logion/controllers/components.ts @@ -94,14 +94,12 @@ export interface components { }; }; /** - * CreateProtectionRequestView - * @description A Protection Request to create + * CreateAccountRecoveryRequestView + * @description An Account Recovery Request to create */ - CreateProtectionRequestView: { - /** @description If this request is a recovery request, tells the address to recover */ - addressToRecover?: string; - /** @description True if the protection request is also a recovery request */ - isRecovery: boolean; + CreateAccountRecoveryRequestView: { + /** @description Tells the address to recover */ + addressToRecover: string; /** @description The SS58 address of the legal officer the request was submitted to */ legalOfficerAddress: string; /** @description The SS58 address of the other legal officer the request was submitted to */ @@ -113,36 +111,23 @@ export interface components { requesterIdentityLoc: string; }; /** - * AcceptProtectionRequestView - * @description Parameters for Protection Request's acceptance + * FetchAccountRecoveryRequestsResponseView + * @description The fetched Account Recovery Requests */ - UpdateProtectionRequestView: { - /** @description The SS58 address of the other legal officer a new request is submitted to */ - otherLegalOfficerAddress?: string; + FetchAccountRecoveryRequestsResponseView: { + /** @description The Account Recovery Requests matching provided specification */ + requests?: components["schemas"]["AccountRecoveryRequestView"][]; }; /** - * FetchProtectionRequestsResponseView - * @description The fetched Protection Requests + * FetchAccountRecoveryRequestsSpecificationView + * @description The specification for fetching Account Recovery Requests */ - FetchProtectionRequestsResponseView: { - /** @description The Protection Requests matching provided specification */ - requests?: components["schemas"]["ProtectionRequestView"][]; - }; - /** - * FetchProtectionRequestsSpecificationView - * @description The specification for fetching Protection Requests - */ - FetchProtectionRequestsSpecificationView: { - /** @description The statuses of expected Protection Requests */ + FetchAccountRecoveryRequestsSpecificationView: { + /** @description The statuses of expected Account Recovery Requests */ statuses?: ("ACCEPTED" | "PENDING" | "REJECTED" | "ACTIVATED" | "CANCELLED" | "REJECTED_CANCELLED" | "ACCEPTED_CANCELLED")[]; - /** - * @description The kind of protection request to be returned - * @enum {string} - */ - kind?: "ANY" | "PROTECTION_ONLY" | "RECOVERY"; - /** @description The SS58 address of the requester in expected Protection Requests */ + /** @description The SS58 address of the requester in expected Account Recovery Requests */ requesterAddress?: string; - /** @description The SS58 address of the legal officer in expected Protection Requests */ + /** @description The SS58 address of the legal officer in expected Account Recovery Requests */ legalOfficerAddress?: string; }; /** @@ -196,10 +181,10 @@ export interface components { postalCode?: string; }; /** - * ProtectionRequestView - * @description Information about the created Protection Request + * AccountRecoveryRequestView + * @description Information about the created Account Recovery Request */ - ProtectionRequestView: { + AccountRecoveryRequestView: { /** @description If this request is a recovery request, tells the address to recover */ addressToRecover?: string; /** @@ -210,11 +195,9 @@ export interface components { decision?: components["schemas"]["LegalOfficerDecisionView"]; /** * Format: uuid - * @description The ID of created Protection Request + * @description The ID of created Account Recovery Request */ id?: string; - /** @description True if the protection request is also a recovery request */ - isRecovery?: boolean; /** @description The SS58 address of the legal officer the request was submitted to */ legalOfficerAddress?: string; /** @description The SS58 address of the other legal officer the request was submitted to */ @@ -804,7 +787,7 @@ export interface components { }; /** * CreateVaultTransferRequestView - * @description A Protection Request to create + * @description A Account Recovery Request to create */ CreateVaultTransferRequestView: { /** @description The origin SS58 address of the transfer. In case of a regular vault-out transfer, this equals to requesterAddress. In case of a vault recovery, this equals to the recovered account */ @@ -822,10 +805,10 @@ export interface components { }; /** * FetchVaultTransferRequestsResponseView - * @description The fetched Protection Requests + * @description The fetched Account Recovery Requests */ FetchVaultTransferRequestsResponseView: { - /** @description The Protection Requests matching provided specification */ + /** @description The Account Recovery Requests matching provided specification */ requests?: components["schemas"]["VaultTransferRequestView"][]; }; /** @@ -861,7 +844,7 @@ export interface components { }; /** * VaultTransferRequestView - * @description Information about the created Protection Request + * @description Information about the created Account Recovery Request */ VaultTransferRequestView: { /** @@ -872,7 +855,7 @@ export interface components { decision?: components["schemas"]["VaultTransferRequestDecisionDecisionView"]; /** * Format: uuid - * @description The ID of created Protection Request + * @description The ID of created Account Recovery Request */ id?: string; /** @description The origin SS58 address of the transfer. In case of a regular vault-out transfer, this equals to requesterAddress. In case of a vault recovery, this equals to the recovered account */ @@ -891,7 +874,7 @@ export interface components { }; /** * RejectVaultTransferRequestView - * @description The Protection Request to reject + * @description The Account Recovery Request to reject */ RejectVaultTransferRequestView: { /** @description The rejection reason */ @@ -1082,7 +1065,7 @@ export interface components { type: components["schemas"]["RecoveryRequestType"]; /** * Format: uuid - * @description The ID of created Protection Request + * @description The ID of created Account Recovery Request */ id: string; /** @description If REJECTED, the reason motivating the rejection. */ diff --git a/src/logion/controllers/recovery.controller.ts b/src/logion/controllers/recovery.controller.ts index 576b5a8d..1b76bfae 100644 --- a/src/logion/controllers/recovery.controller.ts +++ b/src/logion/controllers/recovery.controller.ts @@ -11,7 +11,7 @@ import { Controller, HttpPut, ApiController, Async } from "dinoloop"; import { components } from "./components.js"; import { SecretRecoveryRequestRepository, SecretRecoveryRequestAggregateRoot } from "../model/secret_recovery.model.js"; import { LocRequestAdapter } from "./adapters/locrequestadapter.js"; -import { FetchProtectionRequestsSpecification, ProtectionRequestAggregateRoot, ProtectionRequestRepository } from "../model/protectionrequest.model.js"; +import { FetchAccountRecoveryRequestsSpecification, AccountRecoveryRequestAggregateRoot, AccountRecoveryRepository } from "../model/account_recovery.model.js"; type RecoveryRequestView = components["schemas"]["RecoveryRequestView"]; type RecoveryRequestsView = components["schemas"]["RecoveryRequestsView"]; @@ -34,7 +34,7 @@ export class RecoveryController extends ApiController { constructor( private authenticationService: AuthenticationService, private secretRecoveryRequestRepository: SecretRecoveryRequestRepository, - private accountRecoveryRequestRepository: ProtectionRequestRepository, + private accountRecoveryRequestRepository: AccountRecoveryRepository, private locRequestAdapter: LocRequestAdapter, ) { super(); @@ -54,7 +54,7 @@ export class RecoveryController extends ApiController { const legalOfficer = authenticatedUser.validAccountId; const accountRecoveryRequests = await this.accountRecoveryRequestRepository.findBy( - new FetchProtectionRequestsSpecification({ + new FetchAccountRecoveryRequestsSpecification({ expectedLegalOfficerAddress: [ legalOfficer ], }) ); @@ -71,7 +71,7 @@ export class RecoveryController extends ApiController { return view; } - private async toAccountRecoveryRequestView(accountRecoveryRequest: ProtectionRequestAggregateRoot): Promise { + private async toAccountRecoveryRequestView(accountRecoveryRequest: AccountRecoveryRequestAggregateRoot): Promise { const description = accountRecoveryRequest.getDescription(); const { userIdentity, userPostalAddress } = requireDefined( await this.locRequestAdapter.getUserPrivateData(description.requesterIdentityLocId) diff --git a/src/logion/controllers/secret_recovery.controller.ts b/src/logion/controllers/secret_recovery.controller.ts index 5e835ee1..aff16185 100644 --- a/src/logion/controllers/secret_recovery.controller.ts +++ b/src/logion/controllers/secret_recovery.controller.ts @@ -194,10 +194,10 @@ export class SecretRecoveryController extends ApiController { static rejectRequest(spec: OpenAPIV3.Document) { const operationObject = spec.paths["/api/secret-recovery/{id}/reject"].post!; - operationObject.summary = "Rejects a Protection Request"; - operationObject.description = "The authenticated user must be one of the legal officers of the protection request"; + operationObject.summary = "Rejects a request"; + operationObject.description = "The authenticated user must be one of the legal officers of the request"; operationObject.requestBody = getRequestBody({ - description: "Protection Request rejection data", + description: "Request rejection data", view: "RejectRecoveryRequestView", }); operationObject.responses = getDefaultResponsesNoContent(); @@ -227,8 +227,8 @@ export class SecretRecoveryController extends ApiController { static acceptRequest(spec: OpenAPIV3.Document) { const operationObject = spec.paths["/api/secret-recovery/{id}/accept"].post!; - operationObject.summary = "Accepts a Protection Request"; - operationObject.description = "The authenticated user must be one of the legal officers of the protection request"; + operationObject.summary = "Accepts a request"; + operationObject.description = "The authenticated user must be one of the legal officers of the request"; operationObject.responses = getDefaultResponsesNoContent(); setPathParameters(operationObject, { 'id': "The ID of the request to accept" }); } diff --git a/src/logion/controllers/vaulttransferrequest.controller.ts b/src/logion/controllers/vaulttransferrequest.controller.ts index 8fc2ae5d..6bbff5aa 100644 --- a/src/logion/controllers/vaulttransferrequest.controller.ts +++ b/src/logion/controllers/vaulttransferrequest.controller.ts @@ -26,7 +26,7 @@ import { import { components } from './components.js'; import { NotificationService } from "../services/notification.service.js"; import { DirectoryService } from "../services/directory.service.js"; -import { ProtectionRequestDescription, ProtectionRequestRepository } from '../model/protectionrequest.model.js'; +import { AccountRecoveryRequestDescription, AccountRecoveryRepository } from '../model/account_recovery.model.js'; import { VaultTransferRequestService } from '../services/vaulttransferrequest.service.js'; import { LocalsObject } from 'pug'; import { UserPrivateData } from "./adapters/locrequestadapter.js"; @@ -58,7 +58,7 @@ export function fillInSpec(spec: OpenAPIV3.Document): void { interface UserData { requesterAddress: ValidAccountId; userPrivateData: UserPrivateData; - activeRecovery?: ProtectionRequestDescription; + activeRecovery?: AccountRecoveryRequestDescription; activeProtection?: TypesRecoveryConfig; } @@ -72,7 +72,7 @@ export class VaultTransferRequestController extends ApiController { private authenticationService: AuthenticationService, private notificationService: NotificationService, private directoryService: DirectoryService, - private protectionRequestRepository: ProtectionRequestRepository, + private accountRecoveryRepository: AccountRecoveryRepository, private vaultTransferRequestService: VaultTransferRequestService, private polkadotService: PolkadotService, private locRequestRepository: LocRequestRepository, @@ -97,11 +97,11 @@ export class VaultTransferRequestController extends ApiController { const origin = ValidAccountId.polkadot(requireDefined(body.origin, () => badRequest("Missing origin"))); const destination = ValidAccountId.polkadot(requireDefined(body.destination, () => badRequest("Missing destination"))); const legalOfficerAddress = await this.directoryService.requireLegalOfficerAddressOnNode(body.legalOfficerAddress); - const protectionRequestDescription = await this.userAuthorizedAndProtected(origin, legalOfficerAddress); + const userData = await this.userAuthorizedAndProtected(origin, legalOfficerAddress); const request = this.vaultTransferRequestFactory.newVaultTransferRequest({ id: uuid(), - requesterAddress: protectionRequestDescription.requesterAddress, + requesterAddress: userData.requesterAddress, legalOfficerAddress, createdOn: moment().toISOString(), amount: BigInt(body.amount!), @@ -115,11 +115,11 @@ export class VaultTransferRequestController extends ApiController { await this.vaultTransferRequestService.add(request); - this.getNotificationInfo(request.getDescription(), protectionRequestDescription.userPrivateData) + this.getNotificationInfo(request.getDescription(), userData.userPrivateData) .then(info => this.notificationService.notify(info.legalOfficerEmail, "vault-transfer-requested", info.data)) .catch(error => logger.error(error)); - return this.adapt(request, protectionRequestDescription.userPrivateData); + return this.adapt(request, userData.userPrivateData); } private async userAuthorizedAndProtected(origin: ValidAccountId, legalOfficerAddress: ValidAccountId): Promise { @@ -134,7 +134,7 @@ export class VaultTransferRequestController extends ApiController { } private async getProtectionAndRecoveryData(requester: ValidAccountId, origin: ValidAccountId, legalOfficerAddress: ValidAccountId): Promise<{ - activeRecovery?: ProtectionRequestDescription, + activeRecovery?: AccountRecoveryRequestDescription, activeProtection?: TypesRecoveryConfig, }> { const activeRecovery = await this.findActiveRecovery(requester, legalOfficerAddress); @@ -157,7 +157,7 @@ export class VaultTransferRequestController extends ApiController { requester: ValidAccountId, origin: ValidAccountId, legalOfficerAddress: ValidAccountId, - activeRecovery?: ProtectionRequestDescription, + activeRecovery?: AccountRecoveryRequestDescription, activeProtection?: TypesRecoveryConfig, ): Promise { if(!activeRecovery && !activeProtection) { @@ -184,18 +184,17 @@ export class VaultTransferRequestController extends ApiController { }; } - private async findActiveRecovery(requesterAddress: ValidAccountId, legalOfficerAddress: ValidAccountId): Promise { - const protectionRequests = await this.protectionRequestRepository.findBy({ + private async findActiveRecovery(requesterAddress: ValidAccountId, legalOfficerAddress: ValidAccountId): Promise { + const requests = await this.accountRecoveryRepository.findBy({ expectedRequesterAddress: requesterAddress, expectedLegalOfficerAddress: [ legalOfficerAddress ], expectedStatuses: [ 'ACTIVATED' ], - kind: 'RECOVERY' }); - if(protectionRequests.length === 0) { + if(requests.length === 0) { return undefined; } else { - return protectionRequests[0].getDescription(); + return requests[0].getDescription(); } } @@ -244,10 +243,10 @@ export class VaultTransferRequestController extends ApiController { }); const vaultTransferRequests = await this.vaultTransferRequestRepository.findBy(specification); - const protectionDescriptions: Record = {}; + const userDataMap: Record = {}; for(let i = 0; i < vaultTransferRequests.length; ++i) { const request = vaultTransferRequests[i].getDescription(); - protectionDescriptions[request.requesterAddress.address] ||= await this.getUserData( + userDataMap[request.requesterAddress.address] ||= await this.getUserData( request.requesterAddress, request.origin, request.legalOfficerAddress @@ -255,8 +254,8 @@ export class VaultTransferRequestController extends ApiController { } const requests = vaultTransferRequests.map(request => { - const protectionDescription = protectionDescriptions[request.getRequester().address]; - return this.adapt(request, protectionDescription.userPrivateData); + const userData = userDataMap[request.getRequester().address]; + return this.adapt(request, userData.userPrivateData); }); return { requests }; } @@ -303,22 +302,18 @@ export class VaultTransferRequestController extends ApiController { }); const description = request.getDescription(); - const protectionRequestDescription = await this.getUserData(description.requesterAddress, description.origin, description.legalOfficerAddress); - this.getNotificationInfo(request.getDescription(), protectionRequestDescription.userPrivateData, request.decision) + const userData = await this.getUserData(description.requesterAddress, description.origin, description.legalOfficerAddress); + this.getNotificationInfo(request.getDescription(), userData.userPrivateData, request.decision) .then(info => this.notificationService.notify(info.userEmail, "vault-transfer-rejected", info.data)) .catch(error => logger.error(error)); - return this.adapt(request, protectionRequestDescription.userPrivateData); + return this.adapt(request, userData.userPrivateData); } static acceptVaultTransferRequest(spec: OpenAPIV3.Document) { const operationObject = spec.paths["/api/vault-transfer-request/{id}/accept"].post!; operationObject.summary = "Accepts a Vault Transfer Request"; operationObject.description = "The authenticated user must be a LLO operating on the current node"; - operationObject.requestBody = getRequestBody({ - description: "Protection Request acceptance data", - view: "AcceptVaultTransferRequestView", - }); operationObject.responses = getDefaultResponses("VaultTransferRequestView"); setPathParameters(operationObject, { 'id': "The ID of the request to accept" }); } @@ -333,12 +328,12 @@ export class VaultTransferRequestController extends ApiController { }); const description = request.getDescription(); - const protectionRequestDescription = await this.getUserData(description.requesterAddress, description.origin, description.legalOfficerAddress); - this.getNotificationInfo(request.getDescription(), protectionRequestDescription.userPrivateData, request.decision) + const userData = await this.getUserData(description.requesterAddress, description.origin, description.legalOfficerAddress); + this.getNotificationInfo(request.getDescription(), userData.userPrivateData, request.decision) .then(info => this.notificationService.notify(info.userEmail, "vault-transfer-accepted", info.data)) .catch(error => logger.error(error)); - return this.adapt(request, protectionRequestDescription.userPrivateData); + return this.adapt(request, userData.userPrivateData); } @Async() @@ -352,12 +347,12 @@ export class VaultTransferRequestController extends ApiController { }); const description = request.getDescription(); - const protectionRequestDescription = await this.getUserData(description.requesterAddress, description.origin, description.legalOfficerAddress); - this.getNotificationInfo(request.getDescription(), protectionRequestDescription.userPrivateData, request.decision) + const userData = await this.getUserData(description.requesterAddress, description.origin, description.legalOfficerAddress); + this.getNotificationInfo(request.getDescription(), userData.userPrivateData, request.decision) .then(info => this.notificationService.notify(info.legalOfficerEmail, "vault-transfer-cancelled", info.data)) .catch(error => logger.error(error)); - return this.adapt(request, protectionRequestDescription.userPrivateData); + return this.adapt(request, userData.userPrivateData); } @Async() @@ -371,11 +366,11 @@ export class VaultTransferRequestController extends ApiController { }); const description = request.getDescription(); - const protectionRequestDescription = await this.getUserData(description.requesterAddress, description.origin, description.legalOfficerAddress); - this.getNotificationInfo(request.getDescription(), protectionRequestDescription.userPrivateData, request.decision) + const userData = await this.getUserData(description.requesterAddress, description.origin, description.legalOfficerAddress); + this.getNotificationInfo(request.getDescription(), userData.userPrivateData, request.decision) .then(info => this.notificationService.notify(info.legalOfficerEmail, "vault-transfer-requested", info.data)) .catch(error => logger.error(error)); - return this.adapt(request, protectionRequestDescription.userPrivateData); + return this.adapt(request, userData.userPrivateData); } } diff --git a/src/logion/migration/1717410755574-AccountRecoveryRequest.ts b/src/logion/migration/1717410755574-AccountRecoveryRequest.ts new file mode 100644 index 00000000..72b903f0 --- /dev/null +++ b/src/logion/migration/1717410755574-AccountRecoveryRequest.ts @@ -0,0 +1,18 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AccountRecoveryRequest1717410755574 implements MigrationInterface { + name = 'AccountRecoveryRequest1717410755574' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "protection_request" DROP COLUMN "is_recovery"`); + await queryRunner.query(`ALTER TABLE "protection_request" RENAME TO "account_recovery_request"`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "account_recovery_request" RENAME TO "protection_request"`); + await queryRunner.query(`ALTER TABLE "protection_request" ADD COLUMN "is_recovery" boolean`); + await queryRunner.query(`UPDATE "protection_request" SET "is_recovery" = TRUE`); + await queryRunner.query(`ALTER TABLE "protection_request" ALTER COLUMN "is_recovery" SET NOT NULL`); + } + +} diff --git a/src/logion/model/protectionrequest.model.ts b/src/logion/model/account_recovery.model.ts similarity index 69% rename from src/logion/model/protectionrequest.model.ts rename to src/logion/model/account_recovery.model.ts index 7fc42e96..6f9b66f2 100644 --- a/src/logion/model/protectionrequest.model.ts +++ b/src/logion/model/account_recovery.model.ts @@ -9,12 +9,10 @@ import { LegalOfficerDecision, LegalOfficerDecisionDescription } from "./decisio const { logger } = Log; -export type ProtectionRequestStatus = 'PENDING' | 'REJECTED' | 'ACCEPTED' | 'ACTIVATED' | 'CANCELLED' | 'REJECTED_CANCELLED' | 'ACCEPTED_CANCELLED'; +export type AccountRecoveryRequestStatus = 'PENDING' | 'REJECTED' | 'ACCEPTED' | 'ACTIVATED' | 'CANCELLED' | 'REJECTED_CANCELLED' | 'ACCEPTED_CANCELLED'; -export type ProtectionRequestKind = 'RECOVERY' | 'PROTECTION_ONLY' | 'ANY'; - -@Entity("protection_request") -export class ProtectionRequestAggregateRoot { +@Entity("account_recovery_request") +export class AccountRecoveryRequestAggregateRoot { reject(reason: string, decisionOn: Moment): void { if(this.status !== 'PENDING') { @@ -39,17 +37,9 @@ export class ProtectionRequestAggregateRoot { this.status = 'ACTIVATED'; } - resubmit() { - if(this.status !== 'REJECTED') { - throw badRequest("Request is not rejected") - } - this.status = "PENDING"; - this.decision!.clear(); - } - cancel() { if(this.status === 'ACTIVATED') { - throw badRequest("Cannot cancel an already activated protection"); + throw badRequest("Cannot cancel when already activated"); } if(this.status === 'PENDING') { this.status = 'CANCELLED'; @@ -60,16 +50,6 @@ export class ProtectionRequestAggregateRoot { } } - updateOtherLegalOfficer(account: ValidAccountId) { - if(this.status === 'ACTIVATED') { - throw badRequest("Cannot update the LO of an already activated protection") - } - if (this.isRecovery) { - throw badRequest("Cannot update the LO of a recovery request") - } - this.otherLegalOfficerAddress = account.getAddress(DB_SS58_PREFIX); - } - @PrimaryColumn({ type: "uuid" }) id?: string; @@ -79,9 +59,6 @@ export class ProtectionRequestAggregateRoot { @Column("timestamp without time zone", { name: "created_on", nullable: true }) createdOn?: string; - @Column("boolean", {name: "is_recovery" }) - isRecovery?: boolean; - @Column({ length: 255, name: "requester_address" }) requesterAddress?: string; @@ -93,7 +70,7 @@ export class ProtectionRequestAggregateRoot { requesterIdentityLocId?: string; @Column({ length: 255 }) - status?: ProtectionRequestStatus; + status?: AccountRecoveryRequestStatus; @Column(() => LegalOfficerDecision, {prefix: ""}) decision?: LegalOfficerDecision; @@ -104,7 +81,7 @@ export class ProtectionRequestAggregateRoot { @Column({ length: 255, name: "other_legal_officer_address" }) otherLegalOfficerAddress?: string; - getDescription(): ProtectionRequestDescription { + getDescription(): AccountRecoveryRequestDescription { return { id: requireDefined(this.id), status: requireDefined(this.status), @@ -113,8 +90,7 @@ export class ProtectionRequestAggregateRoot { legalOfficerAddress: ValidAccountId.polkadot(this.legalOfficerAddress || ""), otherLegalOfficerAddress: ValidAccountId.polkadot(this.otherLegalOfficerAddress || ""), createdOn: requireDefined(this.createdOn), - isRecovery: requireDefined(this.isRecovery), - addressToRecover: this.addressToRecover ? ValidAccountId.polkadot(this.addressToRecover) : null, + addressToRecover: ValidAccountId.polkadot(requireDefined(this.addressToRecover)), }; } @@ -141,53 +117,50 @@ export class ProtectionRequestAggregateRoot { return ValidAccountId.polkadot(this.requesterAddress || "") } - getAddressToRecover(): ValidAccountId | null { + getAddressToRecover(): ValidAccountId { if (this.addressToRecover !== undefined && this.addressToRecover !== null) { return ValidAccountId.polkadot(this.addressToRecover) } else { - return null; + throw new Error("No address to recover"); } } } -export class FetchProtectionRequestsSpecification { +export class FetchAccountRecoveryRequestsSpecification { constructor(builder: { expectedRequesterAddress?: ValidAccountId, expectedLegalOfficerAddress?: ValidAccountId[], - expectedStatuses?: ProtectionRequestStatus[], - kind?: ProtectionRequestKind, + expectedStatuses?: AccountRecoveryRequestStatus[], }) { this.expectedRequesterAddress = builder.expectedRequesterAddress || null; this.expectedLegalOfficerAddress = builder.expectedLegalOfficerAddress || null; this.expectedStatuses = builder.expectedStatuses || []; - this.kind = builder.kind || 'ANY'; } readonly expectedRequesterAddress: ValidAccountId | null; readonly expectedLegalOfficerAddress: ValidAccountId[] | null; - readonly kind: ProtectionRequestKind; - readonly expectedStatuses: ProtectionRequestStatus[]; + readonly expectedStatuses: AccountRecoveryRequestStatus[]; } @injectable() -export class ProtectionRequestRepository { +export class AccountRecoveryRepository { constructor() { - this.repository = appDataSource.getRepository(ProtectionRequestAggregateRoot); + this.repository = appDataSource.getRepository(AccountRecoveryRequestAggregateRoot); } - readonly repository: Repository; + readonly repository: Repository; - public findById(id: string): Promise { + public findById(id: string): Promise { return this.repository.findOneBy({ id }); } - public async save(root: ProtectionRequestAggregateRoot): Promise { + public async save(root: AccountRecoveryRequestAggregateRoot): Promise { await this.repository.save(root); } - public async findBy(specification: FetchProtectionRequestsSpecification): Promise { + public async findBy(specification: FetchAccountRecoveryRequestsSpecification): Promise { const builder = this.repository.createQueryBuilder("request"); let where = (a: string, b?: ObjectLiteral) => builder.where(a, b); @@ -212,14 +185,6 @@ export class ProtectionRequestRepository { where = (a: string, b?: ObjectLiteral) => builder.andWhere(a, b); } - if(specification.kind === 'RECOVERY') { - where("request.is_recovery IS TRUE"); - where = (a: string, b?: ObjectLiteral) => builder.andWhere(a, b); - } else if(specification.kind === 'PROTECTION_ONLY') { - where("request.is_recovery IS FALSE"); - where = (a: string, b?: ObjectLiteral) => builder.andWhere(a, b); - } - if(specification.expectedStatuses.length > 0) { where("request.status IN (:...expectedStatuses)", {expectedStatuses: specification.expectedStatuses}); } @@ -228,31 +193,29 @@ export class ProtectionRequestRepository { } } -export interface ProtectionRequestDescription { +export interface AccountRecoveryRequestDescription { readonly id: string, - readonly status: ProtectionRequestStatus, + readonly status: AccountRecoveryRequestStatus, readonly requesterAddress: ValidAccountId, readonly requesterIdentityLocId: string, readonly legalOfficerAddress: ValidAccountId, readonly otherLegalOfficerAddress: ValidAccountId, readonly createdOn: string, - readonly isRecovery: boolean, - readonly addressToRecover: ValidAccountId | null, + readonly addressToRecover: ValidAccountId, } -export interface NewProtectionRequestParameters { +export interface NewAccountRecoveryRequestParameters { readonly id: string; readonly requesterAddress: ValidAccountId, readonly requesterIdentityLoc: string, readonly legalOfficerAddress: ValidAccountId, readonly otherLegalOfficerAddress: ValidAccountId, readonly createdOn: string, - readonly isRecovery: boolean, - readonly addressToRecover: ValidAccountId | null, + readonly addressToRecover: ValidAccountId, } @injectable() -export class ProtectionRequestFactory { +export class AccountRecoveryRequestFactory { constructor( @@ -260,14 +223,14 @@ export class ProtectionRequestFactory { ) { } - public async newProtectionRequest(params: NewProtectionRequestParameters): Promise { + public async newAccountRecoveryRequest(params: NewAccountRecoveryRequestParameters): Promise { const identityLoc = requireDefined( await this.locRequestRepository.findById(params.requesterIdentityLoc), () => badRequest("Identity LOC not found") ) identityLoc.isValidPolkadotIdentityLocOrThrow(params.requesterAddress, params.legalOfficerAddress); - const root = new ProtectionRequestAggregateRoot(); + const root = new AccountRecoveryRequestAggregateRoot(); root.id = params.id; root.status = 'PENDING'; root.decision = new LegalOfficerDecision(); @@ -276,13 +239,10 @@ export class ProtectionRequestFactory { root.legalOfficerAddress = params.legalOfficerAddress.getAddress(DB_SS58_PREFIX); root.otherLegalOfficerAddress = params.otherLegalOfficerAddress.getAddress(DB_SS58_PREFIX); root.createdOn = params.createdOn; - root.isRecovery = params.isRecovery; - if(root.isRecovery) { - root.addressToRecover = requireDefined( - params.addressToRecover?.getAddress(DB_SS58_PREFIX), - () => badRequest("Recovery requires an address to recover") - ); - } + root.addressToRecover = requireDefined( + params.addressToRecover?.getAddress(DB_SS58_PREFIX), + () => badRequest("Recovery requires an address to recover") + ); return root; } } diff --git a/src/logion/services/accountrecoveryrequest.service.ts b/src/logion/services/accountrecoveryrequest.service.ts new file mode 100644 index 00000000..6cda4d82 --- /dev/null +++ b/src/logion/services/accountrecoveryrequest.service.ts @@ -0,0 +1,51 @@ +import { DefaultTransactional, requireDefined } from "@logion/rest-api-core"; +import { injectable } from "inversify"; +import { AccountRecoveryRequestAggregateRoot, AccountRecoveryRepository } from "../model/account_recovery.model.js"; + +export abstract class AccountRecoveryRequestService { + + protected constructor( + private accountRecoveryRepository: AccountRecoveryRepository, + ) {} + + async add(request: AccountRecoveryRequestAggregateRoot) { + await this.accountRecoveryRepository.save(request); + } + + async update(id: string, mutator: (request: AccountRecoveryRequestAggregateRoot) => Promise) { + const request = requireDefined(await this.accountRecoveryRepository.findById(id)); + await mutator(request); + await this.accountRecoveryRepository.save(request); + return request; + } +} + +@injectable() +export class NonTransactionalAccountRecoveryRequestService extends AccountRecoveryRequestService { + + constructor( + accountRecoveryRepository: AccountRecoveryRepository, + ) { + super(accountRecoveryRepository); + } +} + +@injectable() +export class TransactionalAccountRecoveryRequestService extends AccountRecoveryRequestService { + + constructor( + accountRecoveryRepository: AccountRecoveryRepository, + ) { + super(accountRecoveryRepository); + } + + @DefaultTransactional() + override async add(request: AccountRecoveryRequestAggregateRoot) { + return super.add(request); + } + + @DefaultTransactional() + override async update(id: string, mutator: (request: AccountRecoveryRequestAggregateRoot) => Promise) { + return super.update(id, mutator); + } +} diff --git a/src/logion/services/protectionsynchronization.service.ts b/src/logion/services/accountrecoverysynchronization.service.ts similarity index 63% rename from src/logion/services/protectionsynchronization.service.ts rename to src/logion/services/accountrecoverysynchronization.service.ts index 62964b1d..8217c4d4 100644 --- a/src/logion/services/protectionsynchronization.service.ts +++ b/src/logion/services/accountrecoverysynchronization.service.ts @@ -1,29 +1,29 @@ import { injectable } from 'inversify'; import { Log } from "@logion/rest-api-core"; -import { ProtectionRequestRepository, FetchProtectionRequestsSpecification } from '../model/protectionrequest.model.js'; +import { AccountRecoveryRepository, FetchAccountRecoveryRequestsSpecification } from '../model/account_recovery.model.js'; import { Adapters, ValidAccountId } from '@logion/node-api'; import { JsonExtrinsic, toString } from "./types/responses/Extrinsic.js"; -import { ProtectionRequestService } from './protectionrequest.service.js'; +import { AccountRecoveryRequestService as AccountRecoveryService } from './accountrecoveryrequest.service.js'; import { DirectoryService } from "./directory.service.js"; const { logger } = Log; @injectable() -export class ProtectionSynchronizer { +export class AccountRecoverySynchronizer { constructor( - private protectionRequestRepository: ProtectionRequestRepository, - private protectionRequestService: ProtectionRequestService, + private accountRecoveryRepository: AccountRecoveryRepository, + private accountRecoveryService: AccountRecoveryService, private directoryService: DirectoryService, ) { } - async updateProtectionRequests(extrinsic: JsonExtrinsic): Promise { + async updateAccountRecoveryRequests(extrinsic: JsonExtrinsic): Promise { if (extrinsic.call.section === "verifiedRecovery") { const error = extrinsic.error(); if (error) { - logger.info("updateProtectionRequests() - Skipping extrinsic with error: %s", toString(extrinsic, error)) + logger.info("updateAccountRecoveryRequests() - Skipping extrinsic with error: %s", toString(extrinsic, error)) return } if (extrinsic.call.method === "createRecovery") { @@ -32,15 +32,15 @@ export class ProtectionSynchronizer { const legalOfficer = ValidAccountId.polkadot(legalOfficerAddress); if (await this.directoryService.isLegalOfficerAddressOnNode(legalOfficer)) { const signer = extrinsic.signer!; - const requests = await this.protectionRequestRepository.findBy(new FetchProtectionRequestsSpecification({ + const requests = await this.accountRecoveryRepository.findBy(new FetchAccountRecoveryRequestsSpecification({ expectedRequesterAddress: ValidAccountId.polkadot(signer), expectedLegalOfficerAddress: [ legalOfficer ], expectedStatuses: [ 'ACCEPTED' ], })); for (let j = 0; j < requests.length; ++j) { const request = requests[j]; - logger.info("Setting protection %s activated", request.id); - await this.protectionRequestService.update(request.id!, async request => { + logger.info("Setting account recovery %s activated", request.id); + await this.accountRecoveryService.update(request.id!, async request => { request.setActivated(); }); } diff --git a/src/logion/services/blockconsumption.service.ts b/src/logion/services/blockconsumption.service.ts index 81226140..afb0a7cb 100644 --- a/src/logion/services/blockconsumption.service.ts +++ b/src/logion/services/blockconsumption.service.ts @@ -8,7 +8,7 @@ import { SyncPointAggregateRoot, SyncPointFactory, SyncPointRepository, TRANSACT import { BlockExtrinsicsService } from "./block.service.js"; import { LocSynchronizer } from "./locsynchronization.service.js"; import { TransactionSynchronizer } from "./transactionsync.service.js"; -import { ProtectionSynchronizer } from "./protectionsynchronization.service.js"; +import { AccountRecoverySynchronizer } from "./accountrecoverysynchronization.service.js"; import { ExtrinsicDataExtractor } from "./extrinsic.data.extractor.js"; import { JsonExtrinsic, toStringWithoutError } from "./types/responses/Extrinsic.js"; import { ProgressRateLogger } from "./progressratelogger.js"; @@ -33,7 +33,7 @@ export class BlockConsumer { private syncPointService: SyncPointService, private transactionSynchronizer: TransactionSynchronizer, private locSynchronizer: LocSynchronizer, - private protectionSynchronizer: ProtectionSynchronizer, + private accountRecoverySynchronizer: AccountRecoverySynchronizer, private extrinsicDataExtractor: ExtrinsicDataExtractor, private prometheusService: PrometheusService, private voteSynchronizer: VoteSynchronizer, @@ -148,7 +148,7 @@ export class BlockConsumer { if (extrinsic.call.pallet !== "timestamp") { logger.info("Processing extrinsic: %s", toStringWithoutError(extrinsic)) await this.locSynchronizer.updateLocRequests(extrinsic, timestamp); - await this.protectionSynchronizer.updateProtectionRequests(extrinsic); + await this.accountRecoverySynchronizer.updateAccountRecoveryRequests(extrinsic); await this.voteSynchronizer.updateVotes(extrinsic, timestamp); } } diff --git a/src/logion/services/notification.service.ts b/src/logion/services/notification.service.ts index 3437b6a1..1ff4cacd 100644 --- a/src/logion/services/notification.service.ts +++ b/src/logion/services/notification.service.ts @@ -10,16 +10,9 @@ const { logger } = Log; export type NotificationRecipient = "WalletUser" | "LegalOfficer"; export const templateValues = [ - "protection-requested", - "protection-accepted", - "protection-rejected", - "protection-resubmitted", - "protection-cancelled", - "protection-updated", "recovery-requested", "recovery-accepted", "recovery-rejected", - "recovery-resubmitted", "recovery-cancelled", "loc-requested", "loc-accepted", diff --git a/src/logion/services/protectionrequest.service.ts b/src/logion/services/protectionrequest.service.ts deleted file mode 100644 index 1e42b90c..00000000 --- a/src/logion/services/protectionrequest.service.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { DefaultTransactional, requireDefined } from "@logion/rest-api-core"; -import { injectable } from "inversify"; -import { ProtectionRequestAggregateRoot, ProtectionRequestRepository } from "../model/protectionrequest.model.js"; - -export abstract class ProtectionRequestService { - - protected constructor( - private protectionRequestRepository: ProtectionRequestRepository, - ) {} - - async add(request: ProtectionRequestAggregateRoot) { - await this.protectionRequestRepository.save(request); - } - - async update(id: string, mutator: (request: ProtectionRequestAggregateRoot) => Promise) { - const request = requireDefined(await this.protectionRequestRepository.findById(id)); - await mutator(request); - await this.protectionRequestRepository.save(request); - return request; - } -} - -@injectable() -export class NonTransactionalProtectionRequestService extends ProtectionRequestService { - - constructor( - protectionRequestRepository: ProtectionRequestRepository, - ) { - super(protectionRequestRepository); - } -} - -@injectable() -export class TransactionalProtectionRequestService extends ProtectionRequestService { - - constructor( - protectionRequestRepository: ProtectionRequestRepository, - ) { - super(protectionRequestRepository); - } - - @DefaultTransactional() - override async add(request: ProtectionRequestAggregateRoot) { - return super.add(request); - } - - @DefaultTransactional() - override async update(id: string, mutator: (request: ProtectionRequestAggregateRoot) => Promise) { - return super.update(id, mutator); - } -} diff --git a/src/logion/services/workload.service.ts b/src/logion/services/workload.service.ts index 484bff8a..61a74e58 100644 --- a/src/logion/services/workload.service.ts +++ b/src/logion/services/workload.service.ts @@ -1,7 +1,7 @@ import { injectable } from "inversify"; import { LocRequestRepository } from "../model/locrequest.model.js"; import { FetchVaultTransferRequestsSpecification, VaultTransferRequestRepository } from "../model/vaulttransferrequest.model.js"; -import { FetchProtectionRequestsSpecification, ProtectionRequestRepository } from "../model/protectionrequest.model.js"; +import { FetchAccountRecoveryRequestsSpecification, AccountRecoveryRepository } from "../model/account_recovery.model.js"; import { ValidAccountId } from "@logion/node-api"; @injectable() @@ -10,7 +10,7 @@ export class WorkloadService { constructor( private locRequestRepository: LocRequestRepository, private vaultTransferRequestRepository: VaultTransferRequestRepository, - private protectionRequestRepository: ProtectionRequestRepository, + private accountRecoveryRepository: AccountRecoveryRepository, ) { } @@ -23,14 +23,14 @@ export class WorkloadService { expectedLegalOfficerAddress: addresses, expectedStatuses: [ "PENDING" ], })); - const pendingProtectionRequests = await this.protectionRequestRepository.findBy(new FetchProtectionRequestsSpecification({ + const pendingAccountRecoveryRequests = await this.accountRecoveryRepository.findBy(new FetchAccountRecoveryRequestsSpecification({ expectedLegalOfficerAddress: addresses, expectedStatuses: [ "PENDING" ], })); return addresses.reduce((map, account) => { map[account.address] = pendingLocRequests.filter(request => request.getOwner().equals(account)).length + pendingVaultTransferRequests.filter(request => request.getLegalOfficer().equals(account)).length - + pendingProtectionRequests.filter(request => request.getLegalOfficer().equals(account)).length; + + pendingAccountRecoveryRequests.filter(request => request.getLegalOfficer().equals(account)).length; return map; }, {} as Record); } diff --git a/test/integration/model/protectionrequest.model.spec.ts b/test/integration/model/account_recovery.model.spec.ts similarity index 61% rename from test/integration/model/protectionrequest.model.spec.ts rename to test/integration/model/account_recovery.model.spec.ts index 3878717e..9bee5b01 100644 --- a/test/integration/model/protectionrequest.model.spec.ts +++ b/test/integration/model/account_recovery.model.spec.ts @@ -2,12 +2,11 @@ import { TestDb } from "@logion/rest-api-core"; import { EmbeddablePostalAddress } from "../../../src/logion/model/postaladdress.js"; import { EmbeddableUserIdentity } from "../../../src/logion/model/useridentity.js"; import { - FetchProtectionRequestsSpecification, - ProtectionRequestAggregateRoot, - ProtectionRequestRepository, - ProtectionRequestKind, - ProtectionRequestStatus, -} from "../../../src/logion/model/protectionrequest.model.js"; + FetchAccountRecoveryRequestsSpecification, + AccountRecoveryRequestAggregateRoot, + AccountRecoveryRepository, + AccountRecoveryRequestStatus, +} from "../../../src/logion/model/account_recovery.model.js"; import { ALICE_ACCOUNT, BOB_ACCOUNT } from "../../helpers/addresses.js"; import { LocRequestAggregateRoot } from "../../../src/logion/model/locrequest.model.js"; import { ValidAccountId } from "@logion/node-api"; @@ -15,22 +14,22 @@ import { EmbeddableNullableAccountId, DB_SS58_PREFIX } from "../../../src/logion const { connect, disconnect, checkNumOfRows, executeScript } = TestDb; -describe('ProtectionRequestRepositoryTest', () => { +describe('AccountRecoveryRepository (read)', () => { beforeAll(async () => { - await connect([ ProtectionRequestAggregateRoot ]); - await executeScript("test/integration/model/protection_requests.sql"); - repository = new ProtectionRequestRepository(); + await connect([ AccountRecoveryRequestAggregateRoot ]); + await executeScript("test/integration/model/account_recovery.sql"); + repository = new AccountRecoveryRepository(); }); - let repository: ProtectionRequestRepository; + let repository: AccountRecoveryRepository; afterAll(async () => { await disconnect(); }); it("findByDecision", async () => { - const specification = new FetchProtectionRequestsSpecification({ + const specification = new FetchAccountRecoveryRequestsSpecification({ expectedStatuses: [ 'ACCEPTED', 'REJECTED'], }); @@ -42,7 +41,7 @@ describe('ProtectionRequestRepositoryTest', () => { it("findByRequesterAddressOnly", async () => { let requesterAddress = ValidAccountId.polkadot("5Ew3MyB15VprZrjQVkpQFj8okmc9xLDSEdNhqMMS5cXsqxoW"); - const specification = new FetchProtectionRequestsSpecification({ + const specification = new FetchAccountRecoveryRequestsSpecification({ expectedRequesterAddress: requesterAddress }); @@ -51,34 +50,8 @@ describe('ProtectionRequestRepositoryTest', () => { expect(results.length).toBe(1); }); - it("findRecoveryOnly", async () => { - const specification = new FetchProtectionRequestsSpecification({ - expectedStatuses: ['ACCEPTED', 'REJECTED'], - kind: 'RECOVERY', - }); - - const results = await repository.findBy(specification); - - expect(results.length).toBe(1); - expectAcceptedOrRejected(results); - expectKind(results, 'RECOVERY'); - }); - - it("findProtectionOnly", async () => { - const specification = new FetchProtectionRequestsSpecification({ - expectedStatuses: ['ACCEPTED', 'REJECTED', 'ACTIVATED'], - kind: 'PROTECTION_ONLY', - }); - - const results = await repository.findBy(specification); - - expect(results.length).toBe(3); - expectAcceptedRejectedActivated(results); - expectKind(results, 'PROTECTION_ONLY'); - }); - it("findActivatedOnly", async () => { - const specification = new FetchProtectionRequestsSpecification({ + const specification = new FetchAccountRecoveryRequestsSpecification({ expectedStatuses: [ 'ACTIVATED' ] }); @@ -89,7 +62,7 @@ describe('ProtectionRequestRepositoryTest', () => { }); it("findPendingOnly", async () => { - const specification = new FetchProtectionRequestsSpecification({ + const specification = new FetchAccountRecoveryRequestsSpecification({ expectedStatuses: [ 'PENDING' ], }); @@ -100,7 +73,7 @@ describe('ProtectionRequestRepositoryTest', () => { }); it("finds workload", async () => { - const specification = new FetchProtectionRequestsSpecification({ + const specification = new FetchAccountRecoveryRequestsSpecification({ expectedLegalOfficerAddress: [ ALICE_ACCOUNT, BOB_ACCOUNT ], expectedStatuses: [ "PENDING" ], }); @@ -110,14 +83,14 @@ describe('ProtectionRequestRepositoryTest', () => { }); }); -describe('ProtectionRequestRepositoryTest', () => { +describe('AccountRecoveryRepository (write)', () => { beforeAll(async () => { - await connect([ ProtectionRequestAggregateRoot ]); - repository = new ProtectionRequestRepository(); + await connect([ AccountRecoveryRequestAggregateRoot ]); + repository = new AccountRecoveryRepository(); }); - let repository: ProtectionRequestRepository; + let repository: AccountRecoveryRepository; afterAll(async () => { await disconnect(); @@ -141,9 +114,8 @@ describe('ProtectionRequestRepositoryTest', () => { identityLoc.userPostalAddress.city = 'Paris' identityLoc.userPostalAddress.country = 'France' - const protectionRequest = new ProtectionRequestAggregateRoot() + const protectionRequest = new AccountRecoveryRequestAggregateRoot() protectionRequest.id = '9a7df79e-9d3a-4ef8-b4e1-496bbe30a639' - protectionRequest.isRecovery = false protectionRequest.requesterAddress = identityLoc.getRequester()?.getAddress(DB_SS58_PREFIX); protectionRequest.requesterIdentityLocId = identityLoc.id; @@ -154,29 +126,20 @@ describe('ProtectionRequestRepositoryTest', () => { await repository.save(protectionRequest) // Then await checkNumOfRows(`SELECT * - FROM protection_request + FROM account_recovery_request WHERE id = '${ protectionRequest.id }'`, 1) }) }); -function expectAcceptedOrRejected(results: ProtectionRequestAggregateRoot[]) { +function expectAcceptedOrRejected(results: AccountRecoveryRequestAggregateRoot[]) { results.forEach(request => expect(request.status).toMatch(/ACCEPTED|REJECTED/)); } -function expectAcceptedRejectedActivated(results: ProtectionRequestAggregateRoot[]) { +function expectAcceptedRejectedActivated(results: AccountRecoveryRequestAggregateRoot[]) { results.forEach(request => expect(request.status).toMatch(/ACCEPTED|REJECTED|ACTIVATED/)); } -function expectKind(results: ProtectionRequestAggregateRoot[], kind: ProtectionRequestKind) { - if(kind === 'ANY') { - return; - } - results.forEach(request => { - expect(request.isRecovery!).toBe(kind == 'RECOVERY'); - }); -} - -function expectStatus(results: ProtectionRequestAggregateRoot[], status: ProtectionRequestStatus) { +function expectStatus(results: AccountRecoveryRequestAggregateRoot[], status: AccountRecoveryRequestStatus) { results.forEach(request => { expect(request.status!).toBe(status); }); diff --git a/test/integration/model/account_recovery.sql b/test/integration/model/account_recovery.sql new file mode 100644 index 00000000..14adcd42 --- /dev/null +++ b/test/integration/model/account_recovery.sql @@ -0,0 +1,14 @@ +INSERT INTO account_recovery_request (id, requester_address, legal_officer_address, other_legal_officer_address, requester_identity_loc_id, status) +VALUES ('d9aea58aa7d24a768b74aff7b82380e1', '5Ew3MyB15VprZrjQVkpQFj8okmc9xLDSEdNhqMMS5cXsqxoW', '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', '5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty', 'b826df54-6d31-4dd0-99af-e89e81890143', 'PENDING'); + +INSERT INTO account_recovery_request (id, requester_address, legal_officer_address, other_legal_officer_address, requester_identity_loc_id, status, decision_on) +VALUES ('7ef13bcd867d487a8fc28c58143e0c43', '5CSbpCKSTvZefZYddesUQ9w6NDye2PHbf12MwBZGBgzGeGoo', '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', '5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty', 'b826df54-6d31-4dd0-99af-e89e81890143', 'ACCEPTED', '2021-10-29T11:47:00.000'); + +INSERT INTO account_recovery_request (id, requester_address, legal_officer_address, other_legal_officer_address, requester_identity_loc_id, status, decision_on, reject_reason) +VALUES ('6ef13bcd867d487a8fc28c58143e0c44', '5EvPZRKRatcHusoKB467pDHcFTe3rG1a4eEhVmLxfirn8Cum', '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', '5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty', 'b826df54-6d31-4dd0-99af-e89e81890143', 'REJECTED', '2021-10-29T11:47:00.000', 'Because.'); + +INSERT INTO account_recovery_request (id, requester_address, legal_officer_address, other_legal_officer_address, requester_identity_loc_id, address_to_recover, status, decision_on, reject_reason) +VALUES ('5926be94b2ba416a80fb069bd1e98845', '5EFHLCx7T6cHD75yjTcTV1KBSd9vbzXYVKweoReWbgVFSNhs', '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', '5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty', 'b826df54-6d31-4dd0-99af-e89e81890143', '5EvPZRKRatcHusoKB467pDHcFTe3rG1a4eEhVmLxfirn8Cum', 'REJECTED', '2021-10-29T11:47:00.000', 'Because.'); + +INSERT INTO account_recovery_request (id, requester_address, legal_officer_address, other_legal_officer_address, requester_identity_loc_id, status, decision_on) +VALUES ('6ef13bcd867d487a8fc28c58143e0c45', '5GThAgk5q8fHoDymHuk7vPmbB9LsDK2BpK78GafB8kL2g8xp', '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', '5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty', 'b826df54-6d31-4dd0-99af-e89e81890143', 'ACTIVATED', '2021-10-29T11:47:00.000'); diff --git a/test/integration/model/protection_requests.sql b/test/integration/model/protection_requests.sql deleted file mode 100644 index c3c78105..00000000 --- a/test/integration/model/protection_requests.sql +++ /dev/null @@ -1,14 +0,0 @@ -INSERT INTO protection_request (id, requester_address, legal_officer_address, other_legal_officer_address, requester_identity_loc_id, is_recovery, status) -VALUES ('d9aea58aa7d24a768b74aff7b82380e1', '5Ew3MyB15VprZrjQVkpQFj8okmc9xLDSEdNhqMMS5cXsqxoW', '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', '5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty', 'b826df54-6d31-4dd0-99af-e89e81890143', FALSE, 'PENDING'); - -INSERT INTO protection_request (id, requester_address, legal_officer_address, other_legal_officer_address, requester_identity_loc_id, is_recovery, status, decision_on) -VALUES ('7ef13bcd867d487a8fc28c58143e0c43', '5CSbpCKSTvZefZYddesUQ9w6NDye2PHbf12MwBZGBgzGeGoo', '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', '5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty', 'b826df54-6d31-4dd0-99af-e89e81890143', FALSE, 'ACCEPTED', '2021-10-29T11:47:00.000'); - -INSERT INTO protection_request (id, requester_address, legal_officer_address, other_legal_officer_address, requester_identity_loc_id, is_recovery, status, decision_on, reject_reason) -VALUES ('6ef13bcd867d487a8fc28c58143e0c44', '5EvPZRKRatcHusoKB467pDHcFTe3rG1a4eEhVmLxfirn8Cum', '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', '5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty', 'b826df54-6d31-4dd0-99af-e89e81890143', FALSE, 'REJECTED', '2021-10-29T11:47:00.000', 'Because.'); - -INSERT INTO protection_request (id, requester_address, legal_officer_address, other_legal_officer_address, requester_identity_loc_id, is_recovery, address_to_recover, status, decision_on, reject_reason) -VALUES ('5926be94b2ba416a80fb069bd1e98845', '5EFHLCx7T6cHD75yjTcTV1KBSd9vbzXYVKweoReWbgVFSNhs', '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', '5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty', 'b826df54-6d31-4dd0-99af-e89e81890143', TRUE, '5EvPZRKRatcHusoKB467pDHcFTe3rG1a4eEhVmLxfirn8Cum', 'REJECTED', '2021-10-29T11:47:00.000', 'Because.'); - -INSERT INTO protection_request (id, requester_address, legal_officer_address, other_legal_officer_address, requester_identity_loc_id, is_recovery, status, decision_on) -VALUES ('6ef13bcd867d487a8fc28c58143e0c45', '5GThAgk5q8fHoDymHuk7vPmbB9LsDK2BpK78GafB8kL2g8xp', '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', '5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty', 'b826df54-6d31-4dd0-99af-e89e81890143', FALSE, 'ACTIVATED', '2021-10-29T11:47:00.000'); diff --git a/test/resources/mail/protection-accepted.pug b/test/resources/mail/protection-accepted.pug deleted file mode 100644 index 12c6f8c3..00000000 --- a/test/resources/mail/protection-accepted.pug +++ /dev/null @@ -1,3 +0,0 @@ -| Your protection is accepted. -| Your protection is accepted. -include /footer.pug diff --git a/test/resources/mail/protection-requested.pug b/test/resources/mail/protection-requested.pug deleted file mode 100644 index b57097dc..00000000 --- a/test/resources/mail/protection-requested.pug +++ /dev/null @@ -1,5 +0,0 @@ -| Your protection is requested -| Dear Legal Officer, -| The following user has requested your protection: -| #{walletUser.firstName} #{walletUser.lastName}(#{protection.requesterAddress.address}) -| diff --git a/test/resources/mail/recovery-accepted.pug b/test/resources/mail/recovery-accepted.pug new file mode 100644 index 00000000..441b6897 --- /dev/null +++ b/test/resources/mail/recovery-accepted.pug @@ -0,0 +1,3 @@ +| Your recovery is accepted. +| Your recovery is accepted. +include /footer.pug diff --git a/test/resources/mail/recovery-requested.pug b/test/resources/mail/recovery-requested.pug new file mode 100644 index 00000000..6c386028 --- /dev/null +++ b/test/resources/mail/recovery-requested.pug @@ -0,0 +1,5 @@ +| Your recovery is requested +| Dear Legal Officer, +| The following user has requested your recovery: +| #{walletUser.firstName} #{walletUser.lastName}(#{recovery.requesterAddress.address}) +| diff --git a/test/unit/controllers/account_recovery.controller.spec.ts b/test/unit/controllers/account_recovery.controller.spec.ts new file mode 100644 index 00000000..5a7e90f0 --- /dev/null +++ b/test/unit/controllers/account_recovery.controller.spec.ts @@ -0,0 +1,545 @@ +import { Container } from 'inversify'; +import { Mock, It, Times } from 'moq.ts'; +import request from 'supertest'; + +import { TestApp } from '@logion/rest-api-core'; + +import { + AccountRecoveryRepository, + AccountRecoveryRequestFactory, + AccountRecoveryRequestAggregateRoot, + NewAccountRecoveryRequestParameters, + AccountRecoveryRequestDescription, +} from '../../../src/logion/model/account_recovery.model.js'; +import { ALICE, BOB, BOB_ACCOUNT, ALICE_ACCOUNT } from '../../helpers/addresses.js'; +import { AccountRecoveryController } from '../../../src/logion/controllers/account_recovery.controller.js'; +import { NotificationService, Template } from "../../../src/logion/services/notification.service.js"; +import moment from "moment"; +import { DirectoryService } from "../../../src/logion/services/directory.service.js"; +import { notifiedLegalOfficer } from "../services/notification-test-data.js"; +import { UserIdentity } from '../../../src/logion/model/useridentity.js'; +import { PostalAddress } from '../../../src/logion/model/postaladdress.js'; +import { NonTransactionalAccountRecoveryRequestService, AccountRecoveryRequestService } from '../../../src/logion/services/accountrecoveryrequest.service.js'; +import { LocRequestAggregateRoot, LocRequestRepository } from "../../../src/logion/model/locrequest.model.js"; +import { LocRequestAdapter } from "../../../src/logion/controllers/adapters/locrequestadapter.js"; +import { ValidAccountId } from "@logion/node-api"; +import { DB_SS58_PREFIX, EmbeddableNullableAccountId } from "../../../src/logion/model/supportedaccountid.model.js"; +import { LocRequestDescription } from 'src/logion/model/loc_vos.js'; +import { LegalOfficerDecision, LegalOfficerDecisionDescription } from 'src/logion/model/decision.js'; + +const DECISION_TIMESTAMP = "2021-06-10T16:25:23.668294"; +const { mockAuthenticationWithCondition, setupApp, mockLegalOfficerOnNode, mockAuthenticationWithAuthenticatedUser, mockAuthenticatedUser } = TestApp; + +describe('Account Recovery request creation', () => { + + it('success with valid recovery request', async () => { + const app = setupApp( + AccountRecoveryController, + container => mockModelForRecovery(container, ACCOUNT_TO_RECOVER.address), + mockAuthenticationWithAuthenticatedUser(mockAuthenticatedUser(true, REQUESTER)) + ); + + await request(app) + .post('/api/account-recovery') + .send({ + requesterIdentityLoc: REQUESTER_IDENTITY_LOC_ID, + legalOfficerAddress: ALICE, + otherLegalOfficerAddress: BOB, + isRecovery: true, + addressToRecover: ACCOUNT_TO_RECOVER.address, + }) + .expect(200) + .expect('Content-Type', /application\/json/) + .then(response => { + expect(response.body.id).toBeDefined(); + }); + }); + + it('failure with empty request', async () => { + const app = setupApp(AccountRecoveryController, container => mockModelForRecovery(container, ACCOUNT_TO_RECOVER.address)); + + await request(app) + .post('/api/account-recovery') + .send({}) + .expect(500) + .expect('Content-Type', /application\/json/); + }); +}); + +const IDENTITY: UserIdentity = { + email: "john.doe@logion.network", + firstName: "John", + lastName: "Doe", + phoneNumber: "+1234", +}; + +const POSTAL_ADDRESS: PostalAddress = { + line1: "Place de le République Française, 10", + line2: "boite 15", + postalCode: "4000", + city: "Liège", + country: "Belgium" +}; + +const ACCOUNT_TO_RECOVER = ValidAccountId.polkadot("vQvrwS6w8eXorsbsH4cp6YdNtEegZYH9CvhHZizV2p9dPGyDJ"); + +function mockRecoveryRequestModel(container: Container, addressToRecover: ValidAccountId): void { + const repository = new Mock(); + repository.setup(instance => instance.save) + .returns(() => Promise.resolve()); + container.bind(AccountRecoveryRepository).toConstantValue(repository.object()); + + const factory = new Mock(); + const root = mockRecoveryRequest() + mockDecision(root, undefined) + const identityLoc = mockIdentityLoc(REQUESTER); + root.setup(instance => instance.requesterIdentityLocId) + .returns(identityLoc.id) + + + factory.setup(instance => instance.newAccountRecoveryRequest( + It.Is(params => { + return params.addressToRecover !== null + && params.addressToRecover.equals(addressToRecover) + && params.requesterAddress.equals(REQUESTER) + }))) + .returns(Promise.resolve(root.object())); + container.bind(AccountRecoveryRequestFactory).toConstantValue(factory.object()); + mockNotificationAndDirectoryService(container); + + container.bind(AccountRecoveryRequestService).toConstantValue(new NonTransactionalAccountRecoveryRequestService(repository.object())); + container.bind(LocRequestAdapter).toConstantValue(mockLocRequestAdapter()); + container.bind(LocRequestRepository).toConstantValue(mockLocRequestRepository()); +} + +function mockLocRequestAdapter(): LocRequestAdapter { + const locRequestAdapter = new Mock(); + locRequestAdapter.setup(instance => instance.getUserPrivateData(REQUESTER_IDENTITY_LOC_ID)) + .returns(Promise.resolve({ + userIdentity: IDENTITY, + userPostalAddress: POSTAL_ADDRESS, + identityLocId: REQUESTER_IDENTITY_LOC_ID + })) + return locRequestAdapter.object(); +} + +function mockLocRequestRepository(): LocRequestRepository { + const repository = new Mock(); + return repository.object(); +} + +function mockIdentityLoc(requester: ValidAccountId): LocRequestAggregateRoot { + const identityLoc = new Mock(); + const description = { + userIdentity: IDENTITY, + userPostalAddress: POSTAL_ADDRESS, + }; + identityLoc.setup(instance => instance.id) + .returns(REQUESTER_IDENTITY_LOC_ID); + identityLoc.setup(instance => instance.requester).returns(EmbeddableNullableAccountId.from(requester)); + identityLoc.setup(instance => instance.getRequester()).returns(requester); + identityLoc.setup(instance => instance.getDescription()) + .returns(description as LocRequestDescription); + return identityLoc.object(); +} + +function mockModelForRecovery(container: Container, addressToRecover: string): void { + mockRecoveryRequestModel(container, ValidAccountId.polkadot(addressToRecover)); +} + +describe('Account Recovery request fetch', () => { + + it('returns expected response', async () => { + const app = setupApp(AccountRecoveryController, mockModelForFetch); + + await request(app) + .put('/api/account-recovery') + .send({ + requesterAddress: REQUESTER.address, + statuses: ["ACCEPTED", "REJECTED"] + }) + .expect(200) + .expect('Content-Type', /application\/json/) + .then(response => { + expect(response.body.requests).toBeDefined(); + expect(response.body.requests.length).toBe(1); + expect(response.body.requests[0].requesterAddress).toBe(REQUESTER.address); + expect(response.body.requests[0].requesterIdentityLoc).toBe(REQUESTER_IDENTITY_LOC_ID); + expect(response.body.requests[0].legalOfficerAddress).toBe(ALICE_ACCOUNT.address); + expect(response.body.requests[0].otherLegalOfficerAddress).toBe(BOB_ACCOUNT.address); + expect(response.body.requests[0].userIdentity.firstName).toBe("John"); + expect(response.body.requests[0].userIdentity.lastName).toBe("Doe"); + expect(response.body.requests[0].userIdentity.email).toBe("john.doe@logion.network"); + expect(response.body.requests[0].userIdentity.phoneNumber).toBe("+1234"); + expect(response.body.requests[0].userPostalAddress.line1).toBe("Place de le République Française, 10"); + expect(response.body.requests[0].userPostalAddress.line2).toBe("boite 15"); + expect(response.body.requests[0].userPostalAddress.postalCode).toBe("4000"); + expect(response.body.requests[0].userPostalAddress.city).toBe("Liège"); + expect(response.body.requests[0].userPostalAddress.country).toBe("Belgium"); + expect(response.body.requests[0].decision.rejectReason).toBe(REJECT_REASON); + expect(response.body.requests[0].decision.decisionOn).toBe(TIMESTAMP); + expect(response.body.requests[0].createdOn).toBe(TIMESTAMP); + expect(response.body.requests[0].status).toBe("REJECTED"); + }); + }); + + it('fails on authentication failure', async () => { + const mock = mockAuthenticationWithCondition(false); + const app = setupApp(AccountRecoveryController, mockModelForFetch, mock); + + await request(app) + .put('/api/account-recovery') + .send({ + requesterAddress: REQUESTER.address, + legalOfficerAddress: ALICE, + decisionStatuses: ["ACCEPTED", "REJECTED"] + }) + .expect(401); + + }); + + it("fetches recovery information", async () => { + const userMock = mockAuthenticationWithAuthenticatedUser(mockLegalOfficerOnNode(ALICE_ACCOUNT)); + const app = setupApp(AccountRecoveryController, mockModelForReview, userMock); + await request(app) + .put(`/api/account-recovery/${ REQUEST_ID }/recovery-info`) + .expect(200) + .then(response => { + expect(response.body.identity1).toBeDefined(); + expect(response.body.identity1.userIdentity).toEqual(IDENTITY); + expect(response.body.identity1.userPostalAddress).toEqual(POSTAL_ADDRESS); + + expect(response.body.identity2).toBeDefined(); + expect(response.body.identity2.userIdentity).toEqual(IDENTITY); + expect(response.body.identity2.userPostalAddress).toEqual(POSTAL_ADDRESS); + + expect(response.body.type).toBe("ACCOUNT"); + + expect(response.body.accountRecovery).toBeDefined(); + expect(response.body.accountRecovery.address1).toBe(ACCOUNT_TO_RECOVER.address); + expect(response.body.accountRecovery.address2).toBe(REQUESTER.address); + }); + }); +}); + +const REQUESTER = ValidAccountId.polkadot("5H4MvAsobfZ6bBCDyj5dsrWYLrA8HrRzaqa9p61UXtxMhSCY"); +const REQUESTER_IDENTITY_LOC_ID = "77c2fef4-6f1d-44a1-a49d-3485c2eb06ee"; +const TIMESTAMP = "2021-06-10T16:25:23.668294"; +const REJECT_REASON = "Illegal"; + +function mockModelForFetch(container: Container): void { + const repository = new Mock(); + + const recoveryRequest = mockRecoveryRequest() + + mockDecision(recoveryRequest, { + rejectReason: REJECT_REASON, + decisionOn: DECISION_TIMESTAMP + }) + + recoveryRequest.setup(instance => instance.legalOfficerAddress).returns(ALICE_ACCOUNT.getAddress(DB_SS58_PREFIX)); + recoveryRequest.setup(instance => instance.createdOn).returns(TIMESTAMP); + recoveryRequest.setup(instance => instance.addressToRecover).returns(null); + recoveryRequest.setup(instance => instance.status).returns('REJECTED'); + const identityLoc = mockIdentityLoc(REQUESTER); + recoveryRequest.setup(instance => instance.requesterIdentityLocId).returns(identityLoc.id); + + const requests: AccountRecoveryRequestAggregateRoot[] = [ recoveryRequest.object() ]; + repository.setup(instance => instance.findBy) + .returns(() => Promise.resolve(requests)); + repository.setup(instance => instance.findById) + .returns(() => Promise.resolve(recoveryRequest.object())); + container.bind(AccountRecoveryRepository).toConstantValue(repository.object()); + + const factory = new Mock(); + container.bind(AccountRecoveryRequestFactory).toConstantValue(factory.object()); + mockNotificationAndDirectoryService(container) + + container.bind(AccountRecoveryRequestService).toConstantValue(new NonTransactionalAccountRecoveryRequestService(repository.object())); + container.bind(LocRequestAdapter).toConstantValue(mockLocRequestAdapter()); + container.bind(LocRequestRepository).toConstantValue(mockLocRequestRepository()); +} + +function mockModelForReview(container: Container): void { + const repository = new Mock(); + + const recoveryRequest = mockRecoveryRequest() + recoveryRequest.setup(instance => instance.legalOfficerAddress).returns(ALICE_ACCOUNT.getAddress(DB_SS58_PREFIX)); + recoveryRequest.setup(instance => instance.createdOn).returns(TIMESTAMP); + recoveryRequest.setup(instance => instance.addressToRecover).returns(ACCOUNT_TO_RECOVER.address); + recoveryRequest.setup(instance => instance.getAddressToRecover()).returns(ACCOUNT_TO_RECOVER); + recoveryRequest.setup(instance => instance.status).returns('PENDING'); + const identityLoc = mockIdentityLoc(ACCOUNT_TO_RECOVER); + recoveryRequest.setup(instance => instance.requesterIdentityLocId).returns(identityLoc.id); + recoveryRequest.setup(instance => instance.getDescription()).returns({ + addressToRecover: ACCOUNT_TO_RECOVER, + createdOn: moment().toISOString(), + id: REQUEST_ID, + legalOfficerAddress: ALICE_ACCOUNT, + otherLegalOfficerAddress: BOB_ACCOUNT, + requesterAddress: REQUESTER, + requesterIdentityLocId: identityLoc.id!, + status: 'PENDING', + }); + + repository.setup(instance => instance.findById) + .returns(() => Promise.resolve(recoveryRequest.object())); + container.bind(AccountRecoveryRepository).toConstantValue(repository.object()); + + const factory = new Mock(); + container.bind(AccountRecoveryRequestFactory).toConstantValue(factory.object()); + mockNotificationAndDirectoryService(container) + + container.bind(AccountRecoveryRequestService).toConstantValue(new NonTransactionalAccountRecoveryRequestService(repository.object())); + container.bind(LocRequestAdapter).toConstantValue(mockLocRequestAdapter()); + + const locRequestRepository = new Mock(); + locRequestRepository.setup(instance => instance.getValidPolkadotIdentityLoc(ACCOUNT_TO_RECOVER, ALICE_ACCOUNT)) + .returns(Promise.resolve(identityLoc)); + container.bind(LocRequestRepository).toConstantValue(locRequestRepository.object()); +} + +function authenticatedLLONotProtectingUser() { + const authenticatedUser = mockLegalOfficerOnNode(BOB_ACCOUNT); + return mockAuthenticationWithAuthenticatedUser(authenticatedUser); +} + +describe('Account Recovery Request accept', () => { + + it('protecting LLO accepts', async () => { + const app = setupApp(AccountRecoveryController, container => mockModelForAccept(container, true)); + + await request(app) + .post('/api/account-recovery/' + REQUEST_ID + "/accept") + .send({ + locId: "locId" + }) + .expect(200) + .expect('Content-Type', /application\/json/); + + notificationService.verify(instance => instance.notify(IDENTITY.email, "recovery-accepted", It.Is(data => { + return data.recovery.decision.decisionOn === DECISION_TIMESTAMP; + }))) + }); + + it('non-protecting LLO fails to accept', async () => { + const app = setupApp(AccountRecoveryController, container => mockModelForAccept(container, true), authenticatedLLONotProtectingUser()); + + await request(app) + .post('/api/account-recovery/' + REQUEST_ID + "/accept") + .send({ + locId: "locId" + }) + .expect(401) + + notificationService.verify(instance => instance.notify, Times.Never()); + }); +}); + +let notificationService: Mock; + +function mockModelForAccept(container: Container, verifies: boolean): void { + const recoveryRequest = mockRecoveryRequest(); + recoveryRequest.setup(instance => instance.accept(It.IsAny())) + .returns(undefined); + + mockDecision(recoveryRequest, { decisionOn: DECISION_TIMESTAMP} ) + + const repository = new Mock(); + repository.setup(instance => instance.findById(REQUEST_ID)) + .returns(Promise.resolve(recoveryRequest.object())); + if(verifies) { + repository.setup(instance => instance.save) + .returns(() => Promise.resolve()); + } + container.bind(AccountRecoveryRepository).toConstantValue(repository.object()); + + const factory = new Mock(); + container.bind(AccountRecoveryRequestFactory).toConstantValue(factory.object()); + mockNotificationAndDirectoryService(container); + + container.bind(AccountRecoveryRequestService).toConstantValue(new NonTransactionalAccountRecoveryRequestService(repository.object())); + container.bind(LocRequestAdapter).toConstantValue(mockLocRequestAdapter()); + container.bind(LocRequestRepository).toConstantValue(mockLocRequestRepository()); +} + +const REQUEST_ID = "requestId"; + +describe('Account Recovery Request reject', () => { + + it('protecting LLO rejects', async () => { + const app = setupApp(AccountRecoveryController, container => mockModelForReject(container, true)); + + await request(app) + .post('/api/account-recovery/' + REQUEST_ID + "/reject") + .send({ + legalOfficerAddress: ALICE, + rejectReason: REJECT_REASON, + }) + .expect(200) + .expect('Content-Type', /application\/json/); + + notificationService.verify(instance => instance.notify(IDENTITY.email, "recovery-rejected", It.Is(data => { + return data.recovery.decision.rejectReason === REJECT_REASON && + data.recovery.decision.decisionOn === DECISION_TIMESTAMP + }))) + }); + + it('non-protecting LLO fails to reject', async () => { + const app = setupApp(AccountRecoveryController, container => mockModelForReject(container, true), authenticatedLLONotProtectingUser()); + + await request(app) + .post('/api/account-recovery/' + REQUEST_ID + "/reject") + .send({ + legalOfficerAddress: ALICE, + rejectReason: REJECT_REASON, + }) + .expect(401); + + notificationService.verify(instance => instance.notify, Times.Never()); + }); +}); + +describe('Account Recovery user', () => { + + let recoveryRequest: Mock; + let repository = new Mock(); + + + beforeEach(() => { + recoveryRequest = mockRecoveryRequest(); + repository = new Mock(); + }) + + it('cancels', async () => { + const app = setupApp(AccountRecoveryController, container => mockModelForUserCancel(container, recoveryRequest, repository)); + + await request(app) + .post('/api/account-recovery/' + REQUEST_ID + "/cancel") + .send() + .expect(204); + + notificationService.verify(instance => instance.notify("alice@logion.network", "recovery-cancelled", It.IsAny())) + recoveryRequest.verify(instance => instance.cancel()) + repository.verify(instance => instance.save(recoveryRequest.object())); + }); + + it('fails to cancel when auth fails', async () => { + const mock = mockAuthenticationWithCondition(false); + const app = setupApp(AccountRecoveryController, container => mockModelForUserCancel(container, recoveryRequest, repository), mock); + + await request(app) + .post('/api/account-recovery/' + REQUEST_ID + "/cancel") + .send() + .expect(401); + recoveryRequest.verify(instance => instance.cancel(), Times.Never()) + }); + +}) + +function mockDecision(request: Mock, decisionDescription: LegalOfficerDecisionDescription | undefined) { + const decision = new Mock(); + if (decisionDescription) { + decision.setup(instance => instance.rejectReason) + .returns(decisionDescription.rejectReason) + decision.setup(instance => instance.decisionOn) + .returns(decisionDescription.decisionOn) + } + request.setup(instance => instance.decision) + .returns(decision.object()); + request.setup(instance => instance.getDecision()) + .returns(decisionDescription) +} + +function mockModelForReject(container: Container, verifies: boolean): void { + const recoveryRequest = mockRecoveryRequest(); + recoveryRequest.setup(instance => instance.reject).returns(() => {}); + + mockDecision(recoveryRequest, { + rejectReason: REJECT_REASON, + decisionOn: DECISION_TIMESTAMP + }) + + const repository = new Mock(); + repository.setup(instance => instance.findById(REQUEST_ID)) + .returns(Promise.resolve(recoveryRequest.object())); + if(verifies) { + repository.setup(instance => instance.save) + .returns(() => Promise.resolve()); + } + container.bind(AccountRecoveryRepository).toConstantValue(repository.object()); + + const factory = new Mock(); + container.bind(AccountRecoveryRequestFactory).toConstantValue(factory.object()); + mockNotificationAndDirectoryService(container); + + container.bind(AccountRecoveryRequestService).toConstantValue(new NonTransactionalAccountRecoveryRequestService(repository.object())); + container.bind(LocRequestAdapter).toConstantValue(mockLocRequestAdapter()); + container.bind(LocRequestRepository).toConstantValue(mockLocRequestRepository()); +} + +function mockNotificationAndDirectoryService(container: Container) { + notificationService = new Mock(); + notificationService + .setup(instance => instance.notify(It.IsAny(), It.IsAny