diff --git a/resources/mail/recoverable-secret-added.pug b/resources/mail/recoverable-secret-added.pug index ded3eed..75c71d8 100644 --- a/resources/mail/recoverable-secret-added.pug +++ b/resources/mail/recoverable-secret-added.pug @@ -1,7 +1,7 @@ | logion notification - Recoverable secret added | Dear #{walletUser.firstName} #{walletUser.lastName}, | -| You receive this message because you just added a recoverable secret with name #{secretName} to your Identity LOC #{loc.id}. +| You receive this message because you just added a recoverable secret with name #{secret.secretName} to your Identity LOC #{loc.id}. | | 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. | diff --git a/resources/mail/secret-recovery-accepted.pug b/resources/mail/secret-recovery-accepted.pug new file mode 100644 index 0000000..8fcda75 --- /dev/null +++ b/resources/mail/secret-recovery-accepted.pug @@ -0,0 +1,9 @@ +| logion notification - Secret recovery accepted +| Dear #{walletUser.firstName} #{walletUser.lastName}, +| +| You receive this message because your request to recover the secret with name #{secret.secretName} linked to your Identity LOC #{loc.id} has been accepted +| by the legal officer in charge. +| +| 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. +| +include /footer.pug diff --git a/resources/mail/secret-recovery-rejected.pug b/resources/mail/secret-recovery-rejected.pug new file mode 100644 index 0000000..115a6fe --- /dev/null +++ b/resources/mail/secret-recovery-rejected.pug @@ -0,0 +1,10 @@ +| logion notification - Secret recovery rejected +| Dear #{walletUser.firstName} #{walletUser.lastName}, +| +| You receive this message because your request to recover the secret with name #{secret.secretName} linked to your Identity LOC #{loc.id} has been rejected +| by the legal officer in charge for the following reason: +| #{secret.decision.rejectReason} +| +| 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. +| +include /footer.pug diff --git a/resources/mail/secret-recovery-requested-user.pug b/resources/mail/secret-recovery-requested-user.pug index d528559..e0345d4 100644 --- a/resources/mail/secret-recovery-requested-user.pug +++ b/resources/mail/secret-recovery-requested-user.pug @@ -1,7 +1,7 @@ | logion notification - Recoverable secret added | Dear #{walletUser.firstName} #{walletUser.lastName}, | -| You receive this message because you just requested to recover the secret with name #{secretName} linked to your Identity LOC #{loc.id}. +| You receive this message because you just requested to recover the secret with name #{secret.secretName} linked to your Identity LOC #{loc.id}. | | 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. | diff --git a/resources/schemas.json b/resources/schemas.json index f86c997..638a253 100644 --- a/resources/schemas.json +++ b/resources/schemas.json @@ -412,24 +412,51 @@ "title": "ProtectionRequestView", "description": "Information about the created Protection Request" }, + "RecoveryInfoIdentityView": { + "type": "object", + "properties": { + "userIdentity": { + "$ref": "#/components/schemas/UserIdentityView" + }, + "userPostalAddress": { + "$ref": "#/components/schemas/PostalAddressView" + } + } + }, "RecoveryInfoView": { "type": "object", "properties": { - "addressToRecover": { - "type": "string", - "description": "The address to recover" + "identity1": { + "$ref": "#/components/schemas/RecoveryInfoIdentityView" }, - "accountToRecover": { - "$ref": "#/components/schemas/ProtectionRequestView" + "identity2": { + "$ref": "#/components/schemas/RecoveryInfoIdentityView" }, - "recoveryAccount": { - "$ref": "#/components/schemas/ProtectionRequestView" + "type": { + "$ref": "#/components/schemas/RecoveryRequestType" + }, + "accountRecovery": { + "type": "object", + "properties": { + "address1": { + "type": "string", + "description": "The address to recover" + }, + "address2": { + "type": "string", + "description": "The recovering address" + } + } } }, "title": "RecoveryInfoView", - "description": "The new (recovery) and old (to recover) account data" + "description": "The new (recovery) and old (to recover) account data", + "required": [ + "identity2", + "type" + ] }, - "RejectProtectionRequestView": { + "RejectRecoveryRequestView": { "type": "object", "properties": { "rejectReason": { @@ -437,8 +464,8 @@ "description": "The rejection reason" } }, - "title": "RejectProtectionRequestView", - "description": "The Protection Request to reject" + "title": "RejectRecoveryRequestView", + "description": "The Recovery Request to reject" }, "RejectTokenRequestView": { "type": "object", @@ -1955,6 +1982,70 @@ "userIdentity", "userPostalAddress" ] + }, + "RecoveryRequestType": { + "type": "string", + "description": "The recovery request type", + "enum": [ + "ACCOUNT", + "SECRET" + ] + }, + "RecoveryRequestView": { + "type": "object", + "properties": { + "userIdentity": { + "$ref": "#/components/schemas/UserIdentityView" + }, + "userPostalAddress": { + "$ref": "#/components/schemas/PostalAddressView" + }, + "createdOn": { + "type": "string", + "format": "date-time", + "description": "The creation timestamp" + }, + "status": { + "type": "string", + "description": "A recovery request status", + "enum": [ + "ACCEPTED", + "PENDING", + "REJECTED", + "ACTIVATED", + "CANCELLED", + "REJECTED_CANCELLED", + "ACCEPTED_CANCELLED" + ] + }, + "type": { + "$ref": "#/components/schemas/RecoveryRequestType" + }, + "id": { + "type": "string", + "format": "uuid", + "description": "The ID of created Protection Request" + } + }, + "required": [ + "id", + "createdOn", + "status", + "type", + "userIdentity", + "userPostalAddress" + ] + }, + "RecoveryRequestsView": { + "type": "object", + "properties": { + "requests": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RecoveryRequestView" + } + } + } } } } diff --git a/src/logion/app.support.ts b/src/logion/app.support.ts index 581b4f5..45bf61d 100644 --- a/src/logion/app.support.ts +++ b/src/logion/app.support.ts @@ -25,6 +25,7 @@ import { fillInSpec as fillInSpecVote, VoteController } from "./controllers/vote import { fillInSpec as fillInSpecTokensRecord, TokensRecordController } from "./controllers/records.controller.js"; import { fillInSpec as fillInSpecWorkload, WorkloadController } from "./controllers/workload.controller.js"; import { fillInSpec as fillInSpecSecretRecovery, SecretRecoveryController } from "./controllers/secret_recovery.controller.js"; +import { fillInSpec as fillInSpecRecovery, RecoveryController } from "./controllers/recovery.controller.js"; const { logger } = Log; @@ -64,6 +65,7 @@ export function predefinedSpec(spec: OpenAPIV3.Document): OpenAPIV3.Document { fillInSpecTokensRecord(spec); fillInSpecWorkload(spec); fillInSpecSecretRecovery(spec); + fillInSpecRecovery(spec); return spec; } @@ -127,6 +129,7 @@ export function setupApp(expressConfig?: ExpressConfig): Express { dino.registerController(TokensRecordController); dino.registerController(WorkloadController); dino.registerController(SecretRecoveryController); + dino.registerController(RecoveryController); dino.dependencyResolver(AppContainer, (injector, type) => { diff --git a/src/logion/container/app.container.ts b/src/logion/container/app.container.ts index 460e28c..ef118e3 100644 --- a/src/logion/container/app.container.ts +++ b/src/logion/container/app.container.ts @@ -69,6 +69,7 @@ import { SecretRecoveryRequestService, TransactionalSecretRecoveryRequestService } from "../services/secret_recovery.service.js"; +import { RecoveryController } from "../controllers/recovery.controller.js"; const container = new Container({ defaultScope: "Singleton", skipBaseClassChecks: true }); configureContainer(container); @@ -175,5 +176,6 @@ container.bind(VoteController).toSelf().inTransientScope(); container.bind(TokensRecordController).toSelf().inTransientScope(); container.bind(WorkloadController).toSelf().inTransientScope(); container.bind(SecretRecoveryController).toSelf().inTransientScope(); +container.bind(RecoveryController).toSelf().inTransientScope(); export { container as AppContainer }; diff --git a/src/logion/controllers/components.ts b/src/logion/controllers/components.ts index 7df9ea3..157afcc 100644 --- a/src/logion/controllers/components.ts +++ b/src/logion/controllers/components.ts @@ -236,21 +236,30 @@ export interface components { /** @description The postal address of the requester */ userPostalAddress?: components["schemas"]["PostalAddressView"]; }; + RecoveryInfoIdentityView: { + userIdentity?: components["schemas"]["UserIdentityView"]; + userPostalAddress?: components["schemas"]["PostalAddressView"]; + }; /** * RecoveryInfoView * @description The new (recovery) and old (to recover) account data */ RecoveryInfoView: { - /** @description The address to recover */ - addressToRecover?: string; - accountToRecover?: components["schemas"]["ProtectionRequestView"]; - recoveryAccount?: components["schemas"]["ProtectionRequestView"]; + identity1?: components["schemas"]["RecoveryInfoIdentityView"]; + identity2: components["schemas"]["RecoveryInfoIdentityView"]; + type: components["schemas"]["RecoveryRequestType"]; + accountRecovery?: { + /** @description The address to recover */ + address1?: string; + /** @description The recovering address */ + address2?: string; + }; }; /** - * RejectProtectionRequestView - * @description The Protection Request to reject + * RejectRecoveryRequestView + * @description The Recovery Request to reject */ - RejectProtectionRequestView: { + RejectRecoveryRequestView: { /** @description The rejection reason */ rejectReason?: string; }; @@ -1052,6 +1061,34 @@ export interface components { userIdentity: components["schemas"]["UserIdentityView"]; userPostalAddress: components["schemas"]["PostalAddressView"]; }; + /** + * @description The recovery request type + * @enum {string} + */ + RecoveryRequestType: "ACCOUNT" | "SECRET"; + RecoveryRequestView: { + userIdentity: components["schemas"]["UserIdentityView"]; + userPostalAddress: components["schemas"]["PostalAddressView"]; + /** + * Format: date-time + * @description The creation timestamp + */ + createdOn: string; + /** + * @description A recovery request status + * @enum {string} + */ + status: "ACCEPTED" | "PENDING" | "REJECTED" | "ACTIVATED" | "CANCELLED" | "REJECTED_CANCELLED" | "ACCEPTED_CANCELLED"; + type: components["schemas"]["RecoveryRequestType"]; + /** + * Format: uuid + * @description The ID of created Protection Request + */ + id: string; + }; + RecoveryRequestsView: { + requests?: components["schemas"]["RecoveryRequestView"][]; + }; }; responses: never; parameters: never; diff --git a/src/logion/controllers/protectionrequest.controller.ts b/src/logion/controllers/protectionrequest.controller.ts index bbf309e..662d1d4 100644 --- a/src/logion/controllers/protectionrequest.controller.ts +++ b/src/logion/controllers/protectionrequest.controller.ts @@ -13,6 +13,7 @@ import { requireDefined, Log, AuthenticationService, + badRequest, } from '@logion/rest-api-core'; import { @@ -36,9 +37,10 @@ type CreateProtectionRequestView = components["schemas"]["CreateProtectionReques type ProtectionRequestView = components["schemas"]["ProtectionRequestView"]; type FetchProtectionRequestsSpecificationView = components["schemas"]["FetchProtectionRequestsSpecificationView"]; type FetchProtectionRequestsResponseView = components["schemas"]["FetchProtectionRequestsResponseView"]; -type RejectProtectionRequestView = components["schemas"]["RejectProtectionRequestView"]; +type RejectRecoveryRequestView = components["schemas"]["RejectRecoveryRequestView"]; type UpdateProtectionRequestView = components["schemas"]["UpdateProtectionRequestView"]; type RecoveryInfoView = components["schemas"]["RecoveryInfoView"]; +type RecoveryInfoIdentityView = components["schemas"]["RecoveryInfoIdentityView"]; const { logger } = Log; @@ -195,7 +197,7 @@ export class ProtectionRequestController extends ApiController { operationObject.description = "The authenticated user must be one of the legal officers of the protection request"; operationObject.requestBody = getRequestBody({ description: "Protection Request rejection data", - view: "RejectProtectionRequestView", + view: "RejectRecoveryRequestView", }); operationObject.responses = getDefaultResponses("ProtectionRequestView"); setPathParameters(operationObject, { 'id': "The ID of the request to reject" }); @@ -203,7 +205,7 @@ export class ProtectionRequestController extends ApiController { @Async() @HttpPost('/:id/reject') - async rejectProtectionRequest(body: RejectProtectionRequestView, id: string): Promise { + async rejectProtectionRequest(body: RejectRecoveryRequestView, id: string): Promise { const authenticatedUser = await this.authenticationService.authenticatedUserIsLegalOfficerOnNode(this.request); const request = await this.protectionRequestService.update(id, async request => { authenticatedUser.require(user => user.is(request.getLegalOfficer())) @@ -243,8 +245,8 @@ export class ProtectionRequestController extends ApiController { static fetchRecoveryInfo(spec: OpenAPIV3.Document) { const operationObject = spec.paths["/api/protection-request/{id}/recovery-info"].put!; - operationObject.summary = "Fetch all info necessary for the legal officer to accept or reject recovery."; - operationObject.description = "The authentication user must be either the protection requester, the recovery requester, or one of the legal officers"; + 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"); setPathParameters(operationObject, { 'id': "The ID of the recovery request" }); } @@ -254,45 +256,40 @@ export class ProtectionRequestController extends ApiController { async fetchRecoveryInfo(_body: never, id: string): Promise { const authenticatedUser = await this.authenticationService.authenticatedUserIsLegalOfficerOnNode(this.request); - const recovery = await this.protectionRequestRepository.findById(id); - authenticatedUser.require(user => user.is(recovery?.getLegalOfficer())); - if(recovery === null - || !recovery.isRecovery - || recovery.status !== 'PENDING' - || recovery.addressToRecover === null) { - throw new Error("Pending recovery request with address to recover not found"); + const accountRecoveryRequest = await this.protectionRequestRepository.findById(id); + if(accountRecoveryRequest === null + || !accountRecoveryRequest.isRecovery + || accountRecoveryRequest.status !== 'PENDING' + || !accountRecoveryRequest.addressToRecover) { + throw badRequest("Pending recovery request with address to recover not found"); } + authenticatedUser.require(user => user.is(accountRecoveryRequest.getLegalOfficer())); - const addressToRecover = recovery.getDescription().addressToRecover!; - const recoveryUserPrivateData = await this.locRequestAdapter.getUserPrivateData(recovery.requesterIdentityLocId!) - const identityLoc = await this.locRequestRepository.getValidPolkadotIdentityLoc( + const addressToRecover = requireDefined(accountRecoveryRequest.getDescription().addressToRecover); + const identity1Loc = await this.locRequestRepository.getValidPolkadotIdentityLoc( addressToRecover, - recovery.getLegalOfficer() + accountRecoveryRequest.getLegalOfficer() ); - let accountToRecoverUserPrivateData: UserPrivateData | undefined; - if(identityLoc) { - const description = identityLoc.getDescription(); - accountToRecoverUserPrivateData = { - identityLocId: identityLoc?.id, + let identity1: RecoveryInfoIdentityView | undefined; + if(identity1Loc) { + const description = identity1Loc.getDescription(); + identity1 = { userIdentity: description.userIdentity, userPostalAddress: description.userPostalAddress, }; } + const identity2PrivateData = await this.locRequestAdapter.getUserPrivateData(accountRecoveryRequest.requesterIdentityLocId!); return { - addressToRecover: addressToRecover.address, - recoveryAccount: this.adapt(recovery, recoveryUserPrivateData), - accountToRecover: accountToRecoverUserPrivateData ? this.adapt({ - getAddressToRecover: () => null, - id: "dummy", - getLegalOfficer: () => recovery.getLegalOfficer(), - getOtherLegalOfficer: () => recovery.getOtherLegalOfficer(), - getRequester: () => addressToRecover, - requesterIdentityLocId: accountToRecoverUserPrivateData.identityLocId, - status: 'ACCEPTED', - decision: { - - } - }, accountToRecoverUserPrivateData) : undefined, + identity1, + identity2: { + userIdentity: identity2PrivateData.userIdentity, + userPostalAddress: identity2PrivateData.userPostalAddress, + }, + type: "ACCOUNT", + accountRecovery: { + address1: identity1Loc?.getRequester()?.address, + address2: accountRecoveryRequest.getRequester().address, + }, }; } diff --git a/src/logion/controllers/recovery.controller.ts b/src/logion/controllers/recovery.controller.ts new file mode 100644 index 0000000..9427781 --- /dev/null +++ b/src/logion/controllers/recovery.controller.ts @@ -0,0 +1,100 @@ +import { OpenAPIV3 } from "express-oas-generator"; +import { + addTag, + setControllerTag, + requireDefined, + getDefaultResponses, + AuthenticationService, +} from "@logion/rest-api-core"; +import { injectable } from "inversify"; +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"; + +type RecoveryRequestView = components["schemas"]["RecoveryRequestView"]; +type RecoveryRequestsView = components["schemas"]["RecoveryRequestsView"]; + +export function fillInSpec(spec: OpenAPIV3.Document): void { + const tagName = 'Recovery'; + addTag(spec, { + name: tagName, + description: "Handling of Recovery Requests" + }); + setControllerTag(spec, /^\/api\/recovery-requests.*/, tagName); + + RecoveryController.fetchRecoveryRequests(spec); +} + +@injectable() +@Controller('/recovery-requests') +export class RecoveryController extends ApiController { + + constructor( + private authenticationService: AuthenticationService, + private secretRecoveryRequestRepository: SecretRecoveryRequestRepository, + private accountRecoveryRequestRepository: ProtectionRequestRepository, + private locRequestAdapter: LocRequestAdapter, + ) { + super(); + } + + static fetchRecoveryRequests(spec: OpenAPIV3.Document) { + const operationObject = spec.paths["/api/recovery-requests"].put!; + operationObject.summary = "Fetches recovery request"; + operationObject.description = "Only for LLOs on current node"; + operationObject.responses = getDefaultResponses("RecoveryRequestsView"); + } + + @Async() + @HttpPut('') + async fetchRecoveryRequests(): Promise { + const authenticatedUser = await this.authenticationService.authenticatedUserIsLegalOfficerOnNode(this.request); + const legalOfficer = authenticatedUser.validAccountId; + + const accountRecoveryRequests = await this.accountRecoveryRequestRepository.findBy( + new FetchProtectionRequestsSpecification({ + expectedLegalOfficerAddress: [ legalOfficer ], + }) + ); + const secretRecoveryRequests = await this.secretRecoveryRequestRepository.findByLegalOfficer(legalOfficer); + const view: RecoveryRequestsView = { + requests: [], + }; + for(const request of accountRecoveryRequests) { + view.requests?.push(await this.toAccountRecoveryRequestView(request)); + } + for(const request of secretRecoveryRequests) { + view.requests?.push(this.toSecretRecoveryRequestView(request)); + } + return view; + } + + private async toAccountRecoveryRequestView(accountRecoveryRequest: ProtectionRequestAggregateRoot): Promise { + const description = accountRecoveryRequest.getDescription(); + const { userIdentity, userPostalAddress } = requireDefined( + await this.locRequestAdapter.getUserPrivateData(description.requesterIdentityLocId) + ); + return { + createdOn: description.createdOn, + id: description.id, + status: description.status, + type: "ACCOUNT", + userIdentity: requireDefined(userIdentity), + userPostalAddress: requireDefined(userPostalAddress), + }; + } + + private toSecretRecoveryRequestView(secretRecoveryRequest: SecretRecoveryRequestAggregateRoot): RecoveryRequestView { + const description = secretRecoveryRequest.getDescription(); + return { + createdOn: description.createdOn.toISOString(), + id: description.id, + status: description.status, + type: "SECRET", + userIdentity: description.userIdentity, + userPostalAddress: description.userPostalAddress, + }; + } +} diff --git a/src/logion/controllers/secret_recovery.controller.ts b/src/logion/controllers/secret_recovery.controller.ts index 6877f59..00c9b46 100644 --- a/src/logion/controllers/secret_recovery.controller.ts +++ b/src/logion/controllers/secret_recovery.controller.ts @@ -1,4 +1,5 @@ import { OpenAPIV3 } from "express-oas-generator"; +import { v4 as uuid } from "uuid"; import { addTag, setControllerTag, @@ -6,14 +7,17 @@ import { badRequest, getRequestBody, getDefaultResponsesNoContent, - Log + Log, + AuthenticationService, + getDefaultResponses, + setPathParameters } from "@logion/rest-api-core"; import { injectable } from "inversify"; -import { Controller, HttpPost, ApiController, Async, SendsResponse } from "dinoloop"; +import { Controller, HttpPost, ApiController, Async, SendsResponse, HttpPut } from "dinoloop"; import { components } from "./components.js"; -import { SecretRecoveryRequestFactory, SecretRecoveryRequestDescription } from "../model/secret_recovery.model.js"; +import { SecretRecoveryRequestFactory, SecretRecoveryRequestDescription, SecretRecoveryRequestRepository } from "../model/secret_recovery.model.js"; import { SecretRecoveryRequestService } from "../services/secret_recovery.service.js"; -import { LocRequestRepository } from "../model/locrequest.model.js"; +import { LocRequestAggregateRoot, LocRequestRepository } from "../model/locrequest.model.js"; import moment from "moment"; import { NotificationRecipient, Template, NotificationService } from "../services/notification.service.js"; import { UserPrivateData } from "./adapters/locrequestadapter.js"; @@ -22,6 +26,9 @@ import { DirectoryService } from "../services/directory.service.js"; import { ValidAccountId } from "@logion/node-api"; type CreateSecretRecoveryRequestView = components["schemas"]["CreateSecretRecoveryRequestView"]; +type RecoveryInfoView = components["schemas"]["RecoveryInfoView"]; +type RecoveryInfoIdentityView = components["schemas"]["RecoveryInfoIdentityView"]; +type RejectRecoveryRequestView = components["schemas"]["RejectRecoveryRequestView"]; const { logger } = Log; @@ -34,6 +41,9 @@ export function fillInSpec(spec: OpenAPIV3.Document): void { setControllerTag(spec, /^\/api\/secret-recovery.*/, tagName); SecretRecoveryController.createSecretRecoveryRequest(spec); + SecretRecoveryController.fetchRecoveryInfo(spec); + SecretRecoveryController.rejectRequest(spec); + SecretRecoveryController.acceptRequest(spec); } @injectable() @@ -43,9 +53,11 @@ export class SecretRecoveryController extends ApiController { constructor( private secretRecoveryRequestFactory: SecretRecoveryRequestFactory, private secretRecoveryRequestService: SecretRecoveryRequestService, + private secretRecoveryRequestRepository: SecretRecoveryRequestRepository, private locRequestRepository: LocRequestRepository, private directoryService: DirectoryService, private notificationService: NotificationService, + private authenticationService: AuthenticationService, ) { super(); } @@ -87,7 +99,9 @@ export class SecretRecoveryController extends ApiController { city: body.userPostalAddress.city || "", country: body.userPostalAddress.country || "", }; + const id = uuid(); const recoveryRequest = this.secretRecoveryRequestFactory.newSecretRecoveryRequest({ + id, requesterIdentityLocId, legalOfficerAddress: requesterIdentityLoc.getOwner(), challenge, @@ -128,11 +142,111 @@ export class SecretRecoveryController extends ApiController { legalOfficerEMail: legalOfficer.userIdentity.email, userEmail: userIdentity?.email, data: { - secretName: secretRecoveryRequest.secretName, legalOfficer, walletUser: userIdentity, - walletUserPostalAddress: userPostalAddress + walletUserPostalAddress: userPostalAddress, + secret: secretRecoveryRequest, } } } + + static fetchRecoveryInfo(spec: OpenAPIV3.Document) { + const operationObject = spec.paths["/api/secret-recovery/{id}/recovery-info"].put!; + operationObject.summary = "Fetch all info necessary for the legal officer to accept or reject secret recovery request."; + operationObject.description = "The authentication user must be a legal officers on current node"; + operationObject.responses = getDefaultResponses("RecoveryInfoView"); + setPathParameters(operationObject, { 'id': "The ID of the recovery request" }); + } + + @Async() + @HttpPut('/:id/recovery-info') + async fetchRecoveryInfo(_body: never, id: string): Promise { + const authenticatedUser = await this.authenticationService.authenticatedUserIsLegalOfficerOnNode(this.request); + + const secretRecoveryRequest = await this.secretRecoveryRequestRepository.findById(id); + if(secretRecoveryRequest === null) { + throw badRequest("Pending secret recovery request not found"); + } + + const secretRecoveryRequestDescription = secretRecoveryRequest.getDescription(); + const identity1Loc = await this.locRequestRepository.findById(secretRecoveryRequestDescription.requesterIdentityLocId); + let identity1: RecoveryInfoIdentityView | undefined; + if(identity1Loc && identity1Loc.getOwner().equals(authenticatedUser.validAccountId)) { + const description = identity1Loc.getDescription(); + identity1 = { + userIdentity: description.userIdentity, + userPostalAddress: description.userPostalAddress, + }; + } + return { + identity1, + identity2: { + userIdentity: secretRecoveryRequestDescription.userIdentity, + userPostalAddress: secretRecoveryRequestDescription.userPostalAddress, + }, + type: "SECRET", + }; + } + + 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.requestBody = getRequestBody({ + description: "Protection Request rejection data", + view: "RejectRecoveryRequestView", + }); + operationObject.responses = getDefaultResponsesNoContent(); + setPathParameters(operationObject, { 'id': "The ID of the request to reject" }); + } + + @Async() + @HttpPost('/:id/reject') + @SendsResponse() + async rejectRequest(body: RejectRecoveryRequestView, id: string): Promise { + const authenticatedUser = await this.authenticationService.authenticatedUserIsLegalOfficerOnNode(this.request); + let requesterIdentityLoc: LocRequestAggregateRoot | null = null; + const recoveryRequest = await this.secretRecoveryRequestService.update(id, async request => { + requesterIdentityLoc = await this.locRequestRepository.findById(request.getDescription().requesterIdentityLocId); + authenticatedUser.require(user => user.is(requesterIdentityLoc?.getOwner())); + request.reject(body.rejectReason!, moment()); + }); + const description = recoveryRequest.getDescription(); + const userPrivateData: UserPrivateData = { + identityLocId: description.requesterIdentityLocId, + userIdentity: description.userIdentity, + userPostalAddress: description.userPostalAddress, + }; + this.notify("WalletUser", "secret-recovery-rejected", recoveryRequest.getDescription(), requesterIdentityLoc!.getOwner(), userPrivateData); + this.response.sendStatus(204); + } + + 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.responses = getDefaultResponsesNoContent(); + setPathParameters(operationObject, { 'id': "The ID of the request to accept" }); + } + + @Async() + @HttpPost('/:id/accept') + @SendsResponse() + async acceptRequest(_body: never, id: string): Promise { + const authenticatedUser = await this.authenticationService.authenticatedUserIsLegalOfficerOnNode(this.request); + let requesterIdentityLoc: LocRequestAggregateRoot | null = null; + const recoveryRequest = await this.secretRecoveryRequestService.update(id, async request => { + requesterIdentityLoc = await this.locRequestRepository.findById(request.getDescription().requesterIdentityLocId); + authenticatedUser.require(user => user.is(requesterIdentityLoc?.getOwner())); + request.accept(moment()); + }); + const description = recoveryRequest.getDescription(); + const userPrivateData: UserPrivateData = { + identityLocId: description.requesterIdentityLocId, + userIdentity: description.userIdentity, + userPostalAddress: description.userPostalAddress, + }; + this.notify("WalletUser", "secret-recovery-accepted", recoveryRequest.getDescription(), requesterIdentityLoc!.getOwner(), userPrivateData); + this.response.sendStatus(204); + } } diff --git a/src/logion/migration/1716302125635-AddSecretStatus.ts b/src/logion/migration/1716302125635-AddSecretStatus.ts new file mode 100644 index 0000000..a259aaf --- /dev/null +++ b/src/logion/migration/1716302125635-AddSecretStatus.ts @@ -0,0 +1,18 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AddSecretStatus1716302125635 implements MigrationInterface { + name = 'AddSecretStatus1716302125635' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "secret_recovery_request" ADD "status" character varying(255) NOT NULL`); + await queryRunner.query(`ALTER TABLE "secret_recovery_request" ADD "decision_on" TIMESTAMP`); + await queryRunner.query(`ALTER TABLE "secret_recovery_request" ADD "reject_reason" character varying(255)`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "secret_recovery_request" DROP COLUMN "reject_reason"`); + await queryRunner.query(`ALTER TABLE "secret_recovery_request" DROP COLUMN "decision_on"`); + await queryRunner.query(`ALTER TABLE "secret_recovery_request" DROP COLUMN "status"`); + } + +} diff --git a/src/logion/model/decision.ts b/src/logion/model/decision.ts new file mode 100644 index 0000000..b2daf3e --- /dev/null +++ b/src/logion/model/decision.ts @@ -0,0 +1,25 @@ +import { Column } from "typeorm"; +import { Moment } from 'moment'; + +export class LegalOfficerDecision { + + reject(reason: string, decisionOn: Moment): void { + this.decisionOn = decisionOn.toISOString(); + this.rejectReason = reason; + } + + accept(decisionOn: Moment): void { + this.decisionOn = decisionOn.toISOString(); + } + + clear() { + this.decisionOn = undefined; + this.rejectReason = undefined; + } + + @Column("timestamp without time zone", { name: "decision_on", nullable: true }) + decisionOn?: string; + + @Column({ length: 255, name: "reject_reason", nullable: true }) + rejectReason?: string; +} diff --git a/src/logion/model/protectionrequest.model.ts b/src/logion/model/protectionrequest.model.ts index 1dc6ee8..753f804 100644 --- a/src/logion/model/protectionrequest.model.ts +++ b/src/logion/model/protectionrequest.model.ts @@ -5,6 +5,7 @@ import { appDataSource, Log, badRequest, requireDefined } from "@logion/rest-api import { LocRequestRepository } from "./locrequest.model.js"; import { ValidAccountId } from "@logion/node-api"; import { DB_SS58_PREFIX } from "./supportedaccountid.model.js"; +import { LegalOfficerDecision } from "./decision.js"; const { logger } = Log; @@ -12,29 +13,6 @@ export type ProtectionRequestStatus = 'PENDING' | 'REJECTED' | 'ACCEPTED' | 'ACT export type ProtectionRequestKind = 'RECOVERY' | 'PROTECTION_ONLY' | 'ANY'; -export class LegalOfficerDecision { - - reject(reason: string, decisionOn: Moment): void { - this.decisionOn = decisionOn.toISOString(); - this.rejectReason = reason; - } - - accept(decisionOn: Moment): void { - this.decisionOn = decisionOn.toISOString(); - } - - clear() { - this.decisionOn = undefined; - this.rejectReason = undefined; - } - - @Column("timestamp without time zone", { name: "decision_on", nullable: true }) - decisionOn?: string; - - @Column({ length: 255, name: "reject_reason", nullable: true }) - rejectReason?: string; -} - @Entity("protection_request") export class ProtectionRequestAggregateRoot { @@ -128,12 +106,14 @@ export class ProtectionRequestAggregateRoot { getDescription(): ProtectionRequestDescription { return { + id: requireDefined(this.id), + status: requireDefined(this.status), requesterAddress: ValidAccountId.polkadot(this.requesterAddress || ""), requesterIdentityLocId: this.requesterIdentityLocId || "", legalOfficerAddress: ValidAccountId.polkadot(this.legalOfficerAddress || ""), otherLegalOfficerAddress: ValidAccountId.polkadot(this.otherLegalOfficerAddress || ""), - createdOn: this.createdOn!, - isRecovery: this.isRecovery!, + createdOn: requireDefined(this.createdOn), + isRecovery: requireDefined(this.isRecovery), addressToRecover: this.addressToRecover ? ValidAccountId.polkadot(this.addressToRecover) : null, }; } @@ -249,6 +229,8 @@ export class ProtectionRequestRepository { } export interface ProtectionRequestDescription { + readonly id: string, + readonly status: ProtectionRequestStatus, readonly requesterAddress: ValidAccountId, readonly requesterIdentityLocId: string, readonly legalOfficerAddress: ValidAccountId, diff --git a/src/logion/model/secret_recovery.model.ts b/src/logion/model/secret_recovery.model.ts index 07f263e..1ffeef9 100644 --- a/src/logion/model/secret_recovery.model.ts +++ b/src/logion/model/secret_recovery.model.ts @@ -2,12 +2,14 @@ import { Entity, PrimaryColumn, Column, Repository } from "typeorm"; import { EmbeddableUserIdentity, UserIdentity, toUserIdentity } from "./useridentity.js"; import { EmbeddablePostalAddress, PostalAddress, toPostalAddress } from "./postaladdress.js"; import { injectable } from "inversify"; -import { appDataSource } from "@logion/rest-api-core"; +import { appDataSource, requireDefined } from "@logion/rest-api-core"; import { Moment } from "moment"; import { ValidAccountId } from "@logion/node-api"; -import { v4 as uuid } from "uuid"; import { DB_SS58_PREFIX } from "./supportedaccountid.model.js"; import moment from "moment"; +import { LegalOfficerDecision } from "./decision.js"; + +export type SecretRecoveryRequestStatus = 'PENDING' | 'REJECTED' | 'ACCEPTED'; @Entity("secret_recovery_request") export class SecretRecoveryRequestAggregateRoot { @@ -36,25 +38,51 @@ export class SecretRecoveryRequestAggregateRoot { @Column("timestamp without time zone", { name: "created_on" }) createdOn?: Date; + @Column({ length: 255 }) + status?: SecretRecoveryRequestStatus; + + @Column(() => LegalOfficerDecision, {prefix: ""}) + decision?: LegalOfficerDecision; + getDescription(): SecretRecoveryRequestDescription { return { + id: requireDefined(this.id), requesterIdentityLocId: this.requesterIdentityLocId || "", secretName: this.secretName || "", challenge: this.challenge || "", createdOn: moment(this.createdOn), userIdentity: toUserIdentity(this.userIdentity)!, userPostalAddress: toPostalAddress(this.userPostalAddress)!, + status: requireDefined(this.status), + } + } + + reject(reason: string, decisionOn: Moment): void { + if(this.status !== 'PENDING') { + throw new Error("Request is not pending"); + } + this.status = 'REJECTED'; + this.decision!.reject(reason, decisionOn); + } + + accept(decisionOn: Moment): void { + if(this.status !== 'PENDING') { + throw new Error("Request is not pending"); } + this.status = 'ACCEPTED'; + this.decision!.accept(decisionOn); } } export interface SecretRecoveryRequestDescription { - readonly requesterIdentityLocId: string, + readonly id: string; + readonly requesterIdentityLocId: string; readonly secretName: string; readonly challenge: string; readonly userIdentity: UserIdentity; readonly userPostalAddress: PostalAddress; - readonly createdOn: Moment, + readonly createdOn: Moment; + readonly status: SecretRecoveryRequestStatus; } @injectable() @@ -69,10 +97,25 @@ export class SecretRecoveryRequestRepository { async save(setting: SecretRecoveryRequestAggregateRoot): Promise { await this.repository.save(setting); } + + async findByLegalOfficer(accountId: ValidAccountId): Promise { + return this.repository.findBy({ legalOfficerAddress: accountId.getAddress(DB_SS58_PREFIX) }); + } + + async findById(id: string): Promise { + return this.repository.findOneBy({ id }); + } } -export interface NewSecretRecoveryRequestParams extends SecretRecoveryRequestDescription { - legalOfficerAddress: ValidAccountId; +export interface NewSecretRecoveryRequestParams { + readonly id: string; + readonly requesterIdentityLocId: string; + readonly secretName: string; + readonly challenge: string; + readonly userIdentity: UserIdentity; + readonly userPostalAddress: PostalAddress; + readonly createdOn: Moment; + readonly legalOfficerAddress: ValidAccountId; } @injectable() @@ -80,7 +123,7 @@ export class SecretRecoveryRequestFactory { newSecretRecoveryRequest(params: NewSecretRecoveryRequestParams): SecretRecoveryRequestAggregateRoot { const root = new SecretRecoveryRequestAggregateRoot(); - root.id = uuid(); + root.id = params.id; root.requesterIdentityLocId = params.requesterIdentityLocId; root.legalOfficerAddress = params.legalOfficerAddress.getAddress(DB_SS58_PREFIX); root.secretName = params.secretName; @@ -88,6 +131,8 @@ export class SecretRecoveryRequestFactory { root.userIdentity = EmbeddableUserIdentity.from(params.userIdentity); root.userPostalAddress = EmbeddablePostalAddress.from(params.userPostalAddress); root.createdOn = params.createdOn.toDate(); + root.status = "PENDING"; + root.decision = new LegalOfficerDecision(); return root; } } diff --git a/src/logion/services/notification.service.ts b/src/logion/services/notification.service.ts index e3404ad..3437b6a 100644 --- a/src/logion/services/notification.service.ts +++ b/src/logion/services/notification.service.ts @@ -39,6 +39,8 @@ export const templateValues = [ "recoverable-secret-added", "secret-recovery-requested-legal-officer", "secret-recovery-requested-user", + "secret-recovery-rejected", + "secret-recovery-accepted", ] as const; export type Template = typeof templateValues[number]; diff --git a/src/logion/services/secret_recovery.service.ts b/src/logion/services/secret_recovery.service.ts index 51d4c7b..6eeaede 100644 --- a/src/logion/services/secret_recovery.service.ts +++ b/src/logion/services/secret_recovery.service.ts @@ -2,7 +2,7 @@ import { SecretRecoveryRequestRepository, SecretRecoveryRequestAggregateRoot } from "../model/secret_recovery.model.js"; -import { DefaultTransactional } from "@logion/rest-api-core"; +import { DefaultTransactional, requireDefined } from "@logion/rest-api-core"; import { injectable } from "inversify"; export abstract class SecretRecoveryRequestService { @@ -15,6 +15,13 @@ export abstract class SecretRecoveryRequestService { async add(request: SecretRecoveryRequestAggregateRoot) { await this.secretRecoveryRequestRepository.save(request); } + + async update(id: string, mutator: (item: SecretRecoveryRequestAggregateRoot) => Promise): Promise { + const item = requireDefined(await this.secretRecoveryRequestRepository.findById(id)); + await mutator(item); + await this.secretRecoveryRequestRepository.save(item); + return item; + } } @injectable() @@ -30,6 +37,11 @@ export class TransactionalSecretRecoveryRequestService extends SecretRecoveryReq override async add(request: SecretRecoveryRequestAggregateRoot): Promise { return super.add(request); } + + @DefaultTransactional() + override update(id: string, mutator: (item: SecretRecoveryRequestAggregateRoot) => Promise): Promise { + return super.update(id, mutator); + } } @injectable() diff --git a/test/unit/controllers/protectionrequest.controller.spec.ts b/test/unit/controllers/protectionrequest.controller.spec.ts index c81f72b..6f2bf46 100644 --- a/test/unit/controllers/protectionrequest.controller.spec.ts +++ b/test/unit/controllers/protectionrequest.controller.spec.ts @@ -10,7 +10,6 @@ import { ProtectionRequestAggregateRoot, NewProtectionRequestParameters, ProtectionRequestDescription, - LegalOfficerDecision, LegalOfficerDecisionDescription, } from '../../../src/logion/model/protectionrequest.model.js'; import { ALICE, BOB, CHARLY, BOB_ACCOUNT, ALICE_ACCOUNT, CHARLY_ACCOUNT } from '../../helpers/addresses.js'; @@ -25,8 +24,9 @@ import { NonTransactionalProtectionRequestService, ProtectionRequestService } fr 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 } from "../../../src/logion/model/supportedaccountid.model.js"; +import { DB_SS58_PREFIX, EmbeddableNullableAccountId } from "../../../src/logion/model/supportedaccountid.model.js"; import { LocRequestDescription } from 'src/logion/model/loc_vos.js'; +import { LegalOfficerDecision } from 'src/logion/model/decision.js'; const DECISION_TIMESTAMP = "2021-06-10T16:25:23.668294"; const { mockAuthenticationWithCondition, setupApp, mockLegalOfficerOnNode, mockAuthenticationWithAuthenticatedUser, mockAuthenticatedUser } = TestApp; @@ -57,10 +57,9 @@ describe('createProtectionRequest', () => { }); it('success with valid recovery request', async () => { - const addressToRecover = "vQvrwS6w8eXorsbsH4cp6YdNtEegZYH9CvhHZizV2p9dPGyDJ"; const app = setupApp( ProtectionRequestController, - container => mockModelForRecovery(container, addressToRecover), + container => mockModelForRecovery(container, ACCOUNT_TO_RECOVER.address), mockAuthenticationWithAuthenticatedUser(mockAuthenticatedUser(true, REQUESTER)) ); @@ -71,7 +70,7 @@ describe('createProtectionRequest', () => { legalOfficerAddress: ALICE, otherLegalOfficerAddress: BOB, isRecovery: true, - addressToRecover, + addressToRecover: ACCOUNT_TO_RECOVER.address, }) .expect(200) .expect('Content-Type', /application\/json/) @@ -106,6 +105,8 @@ const POSTAL_ADDRESS: PostalAddress = { country: "Belgium" }; +const ACCOUNT_TO_RECOVER = ValidAccountId.polkadot("vQvrwS6w8eXorsbsH4cp6YdNtEegZYH9CvhHZizV2p9dPGyDJ"); + function mockModelForRequest(container: Container): void { mockProtectionRequestModel(container, false, null); } @@ -119,7 +120,7 @@ function mockProtectionRequestModel(container: Container, isRecovery: boolean, a const factory = new Mock(); const root = mockProtectionRequest() mockDecision(root, undefined) - const identityLoc = mockIdentityLoc(); + const identityLoc = mockIdentityLoc(REQUESTER); root.setup(instance => instance.requesterIdentityLocId) .returns(identityLoc.id) @@ -165,17 +166,19 @@ function mockLocRequestRepository(): LocRequestRepository { return repository.object(); } -function mockIdentityLoc(): LocRequestAggregateRoot { +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() + .returns(description as LocRequestDescription); + return identityLoc.object(); } function mockModelForRecovery(container: Container, addressToRecover: string): void { @@ -234,6 +237,28 @@ describe('fetchProtectionRequests', () => { }); + it("fetches recovery information", async () => { + const userMock = mockAuthenticationWithAuthenticatedUser(mockLegalOfficerOnNode(ALICE_ACCOUNT)); + const app = setupApp(ProtectionRequestController, mockModelForReview, userMock); + await request(app) + .put(`/api/protection-request/${ 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"); @@ -256,12 +281,14 @@ function mockModelForFetch(container: Container): void { protectionRequest.setup(instance => instance.isRecovery).returns(false); protectionRequest.setup(instance => instance.addressToRecover).returns(null); protectionRequest.setup(instance => instance.status).returns('REJECTED'); - const identityLoc = mockIdentityLoc(); + const identityLoc = mockIdentityLoc(REQUESTER); protectionRequest.setup(instance => instance.requesterIdentityLocId).returns(identityLoc.id); const requests: ProtectionRequestAggregateRoot[] = [ protectionRequest.object() ]; repository.setup(instance => instance.findBy) .returns(() => Promise.resolve(requests)); + repository.setup(instance => instance.findById) + .returns(() => Promise.resolve(protectionRequest.object())); container.bind(ProtectionRequestRepository).toConstantValue(repository.object()); const factory = new Mock(); @@ -273,6 +300,47 @@ function mockModelForFetch(container: Container): void { container.bind(LocRequestRepository).toConstantValue(mockLocRequestRepository()); } +function mockModelForReview(container: Container): void { + const repository = new Mock(); + + const protectionRequest = mockProtectionRequest() + protectionRequest.setup(instance => instance.legalOfficerAddress).returns(ALICE_ACCOUNT.getAddress(DB_SS58_PREFIX)); + protectionRequest.setup(instance => instance.createdOn).returns(TIMESTAMP); + protectionRequest.setup(instance => instance.isRecovery).returns(true); + protectionRequest.setup(instance => instance.addressToRecover).returns(ACCOUNT_TO_RECOVER.address); + protectionRequest.setup(instance => instance.getAddressToRecover()).returns(ACCOUNT_TO_RECOVER); + protectionRequest.setup(instance => instance.status).returns('PENDING'); + const identityLoc = mockIdentityLoc(ACCOUNT_TO_RECOVER); + protectionRequest.setup(instance => instance.requesterIdentityLocId).returns(identityLoc.id); + protectionRequest.setup(instance => instance.getDescription()).returns({ + addressToRecover: ACCOUNT_TO_RECOVER, + createdOn: moment().toISOString(), + id: REQUEST_ID, + isRecovery: true, + legalOfficerAddress: ALICE_ACCOUNT, + otherLegalOfficerAddress: BOB_ACCOUNT, + requesterAddress: REQUESTER, + requesterIdentityLocId: identityLoc.id!, + status: 'PENDING', + }); + + repository.setup(instance => instance.findById) + .returns(() => Promise.resolve(protectionRequest.object())); + container.bind(ProtectionRequestRepository).toConstantValue(repository.object()); + + const factory = new Mock(); + container.bind(ProtectionRequestFactory).toConstantValue(factory.object()); + mockNotificationAndDirectoryService(container) + + container.bind(ProtectionRequestService).toConstantValue(new NonTransactionalProtectionRequestService(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); @@ -523,8 +591,10 @@ function mockNotificationAndDirectoryService(container: Container) { function mockProtectionRequest(): Mock { - const identityLoc = mockIdentityLoc(); + const identityLoc = mockIdentityLoc(REQUESTER); const description: ProtectionRequestDescription = { + id: "a7ff4ab6-5bef-4310-9c28-bcbd653565c3", + status: "ACCEPTED", requesterAddress: REQUESTER, requesterIdentityLocId: identityLoc.id!, legalOfficerAddress: ALICE_ACCOUNT, diff --git a/test/unit/controllers/recovery.controller.spec.ts b/test/unit/controllers/recovery.controller.spec.ts new file mode 100644 index 0000000..159abc0 --- /dev/null +++ b/test/unit/controllers/recovery.controller.spec.ts @@ -0,0 +1,146 @@ +import { Container } from 'inversify'; +import { Mock } from 'moq.ts'; +import request from 'supertest'; + +import { TestApp } from '@logion/rest-api-core'; + +import { + ProtectionRequestRepository, + ProtectionRequestAggregateRoot, + ProtectionRequestDescription, +} from '../../../src/logion/model/protectionrequest.model.js'; +import { BOB_ACCOUNT, ALICE_ACCOUNT } from '../../helpers/addresses.js'; +import { UserIdentity } from '../../../src/logion/model/useridentity.js'; +import { PostalAddress } from '../../../src/logion/model/postaladdress.js'; +import { LocRequestAdapter } from "../../../src/logion/controllers/adapters/locrequestadapter.js"; +import { ValidAccountId } from "@logion/node-api"; +import { RecoveryController } from '../../../src/logion/controllers/recovery.controller.js'; +import { SecretRecoveryRequestAggregateRoot, SecretRecoveryRequestDescription, SecretRecoveryRequestRepository } from '../../../src/logion/model/secret_recovery.model.js'; +import { ItIsAccount } from '../../helpers/Mock.js'; +import moment from 'moment'; + +const { setupApp, mockLegalOfficerOnNode, mockAuthenticationWithAuthenticatedUser } = TestApp; + +describe('RecoveryController', () => { + + it('fetchRecoveryRequests', async () => { + const userMock = mockAuthenticationWithAuthenticatedUser(mockLegalOfficerOnNode(ALICE_ACCOUNT)); + const app = setupApp(RecoveryController, mockModelForFetch, userMock); + + await request(app) + .put('/api/recovery-requests') + .expect(200) + .expect('Content-Type', /application\/json/) + .then(response => { + expect(response.body.requests).toBeDefined(); + expect(response.body.requests.length).toBe(2); + + expect(response.body.requests[0].userIdentity).toEqual(IDENTITY); + expect(response.body.requests[0].userPostalAddress).toEqual(POSTAL_ADDRESS); + expect(response.body.requests[0].createdOn).toBe(ACCOUNT_CREATED_ON); + expect(response.body.requests[0].status).toBe("ACCEPTED"); + expect(response.body.requests[0].id).toBe(ACCOUNT_RECOVERY_REQUEST_ID); + expect(response.body.requests[0].type).toBe("ACCOUNT"); + + expect(response.body.requests[1].userIdentity).toEqual(IDENTITY); + expect(response.body.requests[1].userPostalAddress).toEqual(POSTAL_ADDRESS); + expect(response.body.requests[1].createdOn).toBe(SECRET_CREATED_ON.toISOString()); + expect(response.body.requests[1].status).toBe("ACCEPTED"); + expect(response.body.requests[1].id).toBe(SECRET_RECOVERY_REQUEST_ID); + expect(response.body.requests[1].type).toBe("SECRET"); + }); + }); +}); + +function mockModelForFetch(container: Container): void { + const accountRecoveryRequestRepository = new Mock(); + + const protectionRequest = mockProtectionRequest(); + + const requests: ProtectionRequestAggregateRoot[] = [ protectionRequest.object() ]; + accountRecoveryRequestRepository.setup(instance => instance.findBy) + .returns(() => Promise.resolve(requests)); + container.bind(ProtectionRequestRepository).toConstantValue(accountRecoveryRequestRepository.object()); + + container.bind(LocRequestAdapter).toConstantValue(mockLocRequestAdapter()); + + const secretRecoveryRequest = mockSecretRecoveryRequest(); + const secretRecoveryRequestRepository = new Mock(); + secretRecoveryRequestRepository.setup(instance => instance.findByLegalOfficer(ItIsAccount(ALICE_ACCOUNT))) + .returns(Promise.resolve([ secretRecoveryRequest.object() ])); + container.bind(SecretRecoveryRequestRepository).toConstantValue(secretRecoveryRequestRepository.object()); +} + +function mockProtectionRequest(): Mock { + const description: ProtectionRequestDescription = { + id: ACCOUNT_RECOVERY_REQUEST_ID, + status: "ACCEPTED", + requesterAddress: REQUESTER, + requesterIdentityLocId: REQUESTER_IDENTITY_LOC_ID, + legalOfficerAddress: ALICE_ACCOUNT, + isRecovery: false, + otherLegalOfficerAddress: BOB_ACCOUNT, + createdOn: ACCOUNT_CREATED_ON, + addressToRecover: null, + } + const protectionRequest = new Mock() + protectionRequest.setup(instance => instance.getDescription()).returns(description) + protectionRequest.setup(instance => instance.getLegalOfficer()).returns(ALICE_ACCOUNT) + protectionRequest.setup(instance => instance.getOtherLegalOfficer()).returns(BOB_ACCOUNT) + protectionRequest.setup(instance => instance.getRequester()).returns(REQUESTER) + protectionRequest.setup(instance => instance.getAddressToRecover()).returns(null) + return protectionRequest +} + +const REQUESTER = ValidAccountId.polkadot("5H4MvAsobfZ6bBCDyj5dsrWYLrA8HrRzaqa9p61UXtxMhSCY"); + +const ACCOUNT_CREATED_ON = "2021-06-10T16:25:23.668294"; + +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 REQUESTER_IDENTITY_LOC_ID = "77c2fef4-6f1d-44a1-a49d-3485c2eb06ee"; + +const ACCOUNT_RECOVERY_REQUEST_ID = "a7ff4ab6-5bef-4310-9c28-bcbd653565c3"; + +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 mockSecretRecoveryRequest(): Mock { + const description: SecretRecoveryRequestDescription = { + id: SECRET_RECOVERY_REQUEST_ID, + status: "ACCEPTED", + requesterIdentityLocId: REQUESTER_IDENTITY_LOC_ID, + createdOn: SECRET_CREATED_ON, + challenge: "Challenge", + secretName: "Key", + userIdentity: IDENTITY, + userPostalAddress: POSTAL_ADDRESS, + } + const protectionRequest = new Mock() + protectionRequest.setup(instance => instance.getDescription()).returns(description) + return protectionRequest +} + +const SECRET_RECOVERY_REQUEST_ID = "f293127e-8356-47a7-a6b7-480cdd1daabd"; +const SECRET_CREATED_ON = moment(); diff --git a/test/unit/controllers/secret_recovery.controller.spec.ts b/test/unit/controllers/secret_recovery.controller.spec.ts index c756313..2cff147 100644 --- a/test/unit/controllers/secret_recovery.controller.spec.ts +++ b/test/unit/controllers/secret_recovery.controller.spec.ts @@ -1,11 +1,12 @@ -import { TestApp } from "@logion/rest-api-core"; +import { AuthenticationService, TestApp } from "@logion/rest-api-core"; import { Container } from "inversify"; import { SecretRecoveryController } from "../../../src/logion/controllers/secret_recovery.controller.js"; import { SecretRecoveryRequestFactory, - SecretRecoveryRequestDescription, SecretRecoveryRequestAggregateRoot + SecretRecoveryRequestDescription, SecretRecoveryRequestAggregateRoot, + SecretRecoveryRequestRepository } from "../../../src/logion/model/secret_recovery.model.js"; -import { SecretRecoveryRequestService } from "../../../src/logion/services/secret_recovery.service.js"; +import { NonTransactionalSecretRecoveryRequestService, SecretRecoveryRequestService } from "../../../src/logion/services/secret_recovery.service.js"; import { LocRequestRepository, LocRequestAggregateRoot, @@ -20,14 +21,17 @@ import moment from "moment"; import { notifiedLegalOfficer } from "../services/notification-test-data.js"; import { ValidAccountId } from "@logion/node-api"; import { LocalsObject } from "pug"; +import { mockAuthenticationWithAuthenticatedUser } from "@logion/rest-api-core/dist/TestApp.js"; +import { UserIdentity } from "src/logion/model/useridentity.js"; +import { PostalAddress } from "src/logion/model/postaladdress.js"; -const { setupApp } = TestApp; +const { setupApp, mockLegalOfficerOnNode } = TestApp; describe("SecretRecoveryController", () => { it("creates secret recovery request", async () => { - const app = setupApp(SecretRecoveryController, mockDependencies); + const app = setupApp(SecretRecoveryController, mockForCreate); await request(app) .post('/api/secret-recovery') .send({ @@ -36,11 +40,12 @@ describe("SecretRecoveryController", () => { secretName: SECRET_NAME, }) .expect(204); + secretRecoveryRequestRepository.verify(instance => instance.save(secretRecoveryRequest.object())); }) it("fails to create secret recovery request with IDLOC not found", async () => { - const app = setupApp(SecretRecoveryController, mockDependencies); + const app = setupApp(SecretRecoveryController, mockForCreate); const unknownLocId = "0a765ca1-f0a8-450a-b66a-5dfe9de7adc6"; await request(app) .post('/api/secret-recovery') @@ -55,7 +60,7 @@ describe("SecretRecoveryController", () => { it("fails to create secret recovery request with Secret not found", async () => { - const app = setupApp(SecretRecoveryController, mockDependencies); + const app = setupApp(SecretRecoveryController, mockForCreate); await request(app) .post('/api/secret-recovery') .send({ @@ -66,12 +71,68 @@ describe("SecretRecoveryController", () => { .expect(400) .expect(response => expect(response.body.errorMessage).toEqual("Secret not found")); }) + + it("accepts existing secret recovery request", async () => { + const userMock = mockAuthenticationWithAuthenticatedUser(mockLegalOfficerOnNode(ALICE_ACCOUNT)); + const app = setupApp(SecretRecoveryController, mockForUpdate, userMock); + await request(app) + .post(`/api/secret-recovery/${ REQUEST_ID }/accept`) + .expect(204); + secretRecoveryRequest.verify(instance => instance.accept(It.IsAny())); + secretRecoveryRequestRepository.verify(instance => instance.save(secretRecoveryRequest.object())); + }) + + it("rejects existing secret recovery request", async () => { + const userMock = mockAuthenticationWithAuthenticatedUser(mockLegalOfficerOnNode(ALICE_ACCOUNT)); + const app = setupApp(SecretRecoveryController, mockForUpdate, userMock); + await request(app) + .post(`/api/secret-recovery/${ REQUEST_ID }/reject`) + .send({ rejectReason: "Because." }) + .expect(204); + secretRecoveryRequest.verify(instance => instance.reject("Because.", It.IsAny())); + secretRecoveryRequestRepository.verify(instance => instance.save(secretRecoveryRequest.object())); + }) + + it("fetches recovery information", async () => { + const userMock = mockAuthenticationWithAuthenticatedUser(mockLegalOfficerOnNode(ALICE_ACCOUNT)); + const app = setupApp(SecretRecoveryController, mockForFetch, userMock); + await request(app) + .put(`/api/secret-recovery/${ REQUEST_ID }/recovery-info`) + .expect(200) + .then(response => { + expect(response.body.identity1).toBeDefined(); + expect(response.body.identity1.userIdentity).toEqual(USER_IDENTITY); + expect(response.body.identity1.userPostalAddress).toEqual(USER_POSTAL_ADDRESS); + + expect(response.body.identity2).toBeDefined(); + expect(response.body.identity2.userIdentity).toEqual(USER_IDENTITY); + expect(response.body.identity2.userPostalAddress).toEqual(USER_POSTAL_ADDRESS); + + expect(response.body.type).toBe("SECRET"); + }); + }) }) -function mockDependencies(container: Container) { +function mockForCreate(container: Container) { + mockForAll(container); + + secretRecoveryRequest.setup(instance => instance.getDescription()) + .returns({ + ...recoveryRequest, + requesterIdentityLocId: IDENTITY_LOC_ID, + secretName: SECRET_NAME, + createdOn: moment(), + id: REQUEST_ID, + status: "PENDING", + }); + secretRecoveryRequestFactory.setup(instance => instance.newSecretRecoveryRequest(It.IsAny())) + .returns(secretRecoveryRequest.object()); +} + +function mockForAll(container: Container) { createAndBindMocks(container); - const identityLoc = new Mock(); + identityLoc = new Mock(); const secret = new Mock(); secret.setup(instance => instance.name) .returns(SECRET_NAME); @@ -84,19 +145,20 @@ function mockDependencies(container: Container) { locRequestRepository.setup(instance => instance.findById(IDENTITY_LOC_ID)) .returns(Promise.resolve(identityLoc.object())); - const secretRecoveryRequest = new Mock(); secretRecoveryRequest.setup(instance => instance.getDescription()) .returns({ ...recoveryRequest, requesterIdentityLocId: IDENTITY_LOC_ID, secretName: SECRET_NAME, createdOn: moment(), + id: REQUEST_ID, + status: "PENDING", }) secretRecoveryRequestFactory.setup(instance => instance.newSecretRecoveryRequest(It.IsAny())) .returns(secretRecoveryRequest.object()); - secretRecoveryRequestService.setup(instance => instance.add(secretRecoveryRequest.object())) - .returns(Promise.resolve()) + secretRecoveryRequestRepository.setup(instance => instance.save(It.IsAny())) + .returns(Promise.resolve()); mockNotifications(); } @@ -114,35 +176,42 @@ function mockNotifications() { const IDENTITY_LOC_ID = "1f95f7e8-022a-4baf-bc90-4796c493dd69"; const SECRET_NAME = "my-secret"; const CHALLENGE = "my-challenge"; - -const recoveryRequest = { - challenge: CHALLENGE, - userIdentity: { - email: "john.doe@logion.network", - firstName: "John", - lastName: "Doe", - phoneNumber: "+1234", - }, - userPostalAddress: { - line1: "Rue de la Paix", +const REQUEST_ID = "a7ff4ab6-5bef-4310-9c28-bcbd653565c3"; +const USER_IDENTITY: UserIdentity = { + email: "john.doe@logion.network", + firstName: "John", + lastName: "Doe", + phoneNumber: "+1234", +}; +const USER_POSTAL_ADDRESS: PostalAddress = { + line1: "Rue de la Paix", line2: "", postalCode: "00000", city: "Liège", country: "Belgium", - }, +}; + +const recoveryRequest = { + challenge: CHALLENGE, + userIdentity: USER_IDENTITY, + userPostalAddress: USER_POSTAL_ADDRESS, } +let identityLoc: Mock; let secretRecoveryRequestFactory: Mock; -let secretRecoveryRequestService: Mock; +let secretRecoveryRequestRepository: Mock; let locRequestRepository: Mock; let directoryService: Mock; let notificationService: Mock; +let secretRecoveryRequest: Mock; function createAndBindMocks(container: Container) { + secretRecoveryRequest = new Mock(); secretRecoveryRequestFactory = new Mock(); container.bind(SecretRecoveryRequestFactory).toConstantValue(secretRecoveryRequestFactory.object()); - secretRecoveryRequestService = new Mock(); - container.bind(SecretRecoveryRequestService).toConstantValue(secretRecoveryRequestService.object()); + secretRecoveryRequestRepository = new Mock(); + container.bind(SecretRecoveryRequestRepository).toConstantValue(secretRecoveryRequestRepository.object()); + container.bind(SecretRecoveryRequestService).toConstantValue(new NonTransactionalSecretRecoveryRequestService(secretRecoveryRequestRepository.object())); locRequestRepository = new Mock(); container.bind(LocRequestRepository).toConstantValue(locRequestRepository.object()); directoryService = new Mock(); @@ -150,3 +219,52 @@ function createAndBindMocks(container: Container) { notificationService = new Mock(); container.bind(NotificationService).toConstantValue(notificationService.object()); } + +function mockForUpdate(container: Container) { + mockForAll(container); + + secretRecoveryRequest = new Mock(); + secretRecoveryRequest.setup(instance => instance.getDescription()) + .returns({ + ...recoveryRequest, + requesterIdentityLocId: IDENTITY_LOC_ID, + secretName: SECRET_NAME, + createdOn: moment(), + id: REQUEST_ID, + status: "PENDING", + }); + secretRecoveryRequest.setup(instance => instance.accept(It.IsAny())).returns(); + secretRecoveryRequest.setup(instance => instance.reject(It.IsAny(), It.IsAny())).returns(); + + secretRecoveryRequestRepository.setup(instance => instance.findById(REQUEST_ID)) + .returns(Promise.resolve(secretRecoveryRequest.object())); +} + +function mockForFetch(container: Container) { + mockForAll(container); + + secretRecoveryRequest = new Mock(); + secretRecoveryRequest.setup(instance => instance.getDescription()) + .returns({ + ...recoveryRequest, + requesterIdentityLocId: IDENTITY_LOC_ID, + secretName: SECRET_NAME, + createdOn: moment(), + id: REQUEST_ID, + status: "PENDING", + }); + secretRecoveryRequestRepository.setup(instance => instance.findById(REQUEST_ID)) + .returns(Promise.resolve(secretRecoveryRequest.object())); + + identityLoc.setup(instance => instance.getDescription()).returns({ + createdOn: moment().toISOString(), + description: "", + fees: { + + }, + locType: "Identity", + ownerAddress: ALICE_ACCOUNT, + userIdentity: USER_IDENTITY, + userPostalAddress: USER_POSTAL_ADDRESS, + }); +} diff --git a/test/unit/controllers/vaulttransferrequest.controller.spec.ts b/test/unit/controllers/vaulttransferrequest.controller.spec.ts index 958287c..694147b 100644 --- a/test/unit/controllers/vaulttransferrequest.controller.spec.ts +++ b/test/unit/controllers/vaulttransferrequest.controller.spec.ts @@ -347,6 +347,8 @@ const POSTAL_ADDRESS: PostalAddress = { const REQUESTER_IDENTITY_LOC_ID = "77c2fef4-6f1d-44a1-a49d-3485c2eb06ee"; const protectionRequestDescription: ProtectionRequestDescription = { + id: "a7ff4ab6-5bef-4310-9c28-bcbd653565c3", + status: "ACTIVATED", addressToRecover: null, requesterIdentityLocId: REQUESTER_IDENTITY_LOC_ID, legalOfficerAddress: ALICE_ACCOUNT, diff --git a/test/unit/model/protectionrequest.model.spec.ts b/test/unit/model/protectionrequest.model.spec.ts index 58deb72..d039018 100644 --- a/test/unit/model/protectionrequest.model.spec.ts +++ b/test/unit/model/protectionrequest.model.spec.ts @@ -36,7 +36,6 @@ describe('ProtectionRequestFactoryTest', () => { const factory = new ProtectionRequestFactory(locRequestRepository.object()); await expectAsyncToThrow( () => factory.newProtectionRequest({ - id, requesterIdentityLoc: "missing", ...description, }), @@ -140,6 +139,8 @@ const userPostalAddress = { country: "Belgium", }; const description: ProtectionRequestDescription = { + id, + status: "PENDING", requesterAddress: ValidAccountId.polkadot("5Ew3MyB15VprZrjQVkpQFj8okmc9xLDSEdNhqMMS5cXsqxoW"), requesterIdentityLocId: "80124e8a-a7d8-456f-a7be-deb4e0983e87", legalOfficerAddress: ALICE_ACCOUNT, @@ -170,7 +171,6 @@ async function newProtectionRequestUsingFactory(status?: ProtectionRequestStatus const factory = new ProtectionRequestFactory(locRequestRepository.object()); const protectionRequest = await factory.newProtectionRequest({ - id, requesterIdentityLoc: identityLoc.id!, ...description, }); diff --git a/test/unit/model/secret_recovery.model.spec.ts b/test/unit/model/secret_recovery.model.spec.ts index 182e891..cba167e 100644 --- a/test/unit/model/secret_recovery.model.spec.ts +++ b/test/unit/model/secret_recovery.model.spec.ts @@ -1,6 +1,7 @@ -import { SecretRecoveryRequestFactory } from "../../../src/logion/model/secret_recovery.model.js"; +import { SecretRecoveryRequestAggregateRoot, SecretRecoveryRequestFactory } from "../../../src/logion/model/secret_recovery.model.js"; import moment from "moment"; import { ALICE_ACCOUNT } from "../../helpers/addresses.js"; +import { LegalOfficerDecision } from "../../../src/logion/model/decision.js"; describe("SecretRecoveryRequestFactory", () => { @@ -22,6 +23,7 @@ describe("SecretRecoveryRequestFactory", () => { }; const params = { + id: "a7ff4ab6-5bef-4310-9c28-bcbd653565c3", requesterIdentityLocId: "fd61e638-4af0-4ced-b018-4f1c31a91e6e", secretName: "my-secret", challenge: "my-challenge", @@ -31,7 +33,8 @@ describe("SecretRecoveryRequestFactory", () => { legalOfficerAddress: ALICE_ACCOUNT, } const secretRecoveryRequest = factory.newSecretRecoveryRequest(params); - expect(secretRecoveryRequest.id).toBeDefined(); + expect(secretRecoveryRequest.getDescription().id).toBe(params.id); + expect(secretRecoveryRequest.getDescription().status).toBe("PENDING"); expect(secretRecoveryRequest.getDescription().userIdentity).toEqual(params.userIdentity); expect(secretRecoveryRequest.getDescription().userPostalAddress).toEqual(params.userPostalAddress); expect(secretRecoveryRequest.getDescription().challenge).toEqual(params.challenge); @@ -39,3 +42,31 @@ describe("SecretRecoveryRequestFactory", () => { expect(secretRecoveryRequest.getDescription().requesterIdentityLocId).toEqual(params.requesterIdentityLocId); }) }) + +describe("SecretRecoveryRequestAggregateRoot", () => { + + it("accepts", () => { + const request = new SecretRecoveryRequestAggregateRoot(); + request.status = "PENDING"; + request.decision = new LegalOfficerDecision(); + + request.accept(moment()); + + expect(request.status).toBe("ACCEPTED"); + const decision = request.decision; + expect(decision?.decisionOn).toBeDefined(); + }) + + it("rejects", () => { + const request = new SecretRecoveryRequestAggregateRoot(); + request.status = "PENDING"; + request.decision = new LegalOfficerDecision(); + + request.reject("Because.", moment()); + + expect(request.status).toBe("REJECTED"); + const decision = request.decision; + expect(decision?.decisionOn).toBeDefined(); + expect(decision?.rejectReason).toBe("Because."); + }) +}) diff --git a/test/unit/services/notification-test-data.ts b/test/unit/services/notification-test-data.ts index b733f66..a7bd1f9 100644 --- a/test/unit/services/notification-test-data.ts +++ b/test/unit/services/notification-test-data.ts @@ -7,8 +7,12 @@ import { LegalOfficer } from "../../../src/logion/model/legalofficer.model.js"; import { VaultTransferRequestDescription } from "src/logion/model/vaulttransferrequest.model.js"; import { ValidAccountId } from "@logion/node-api"; import { LocRequestDecision, LocRequestDescription } from "src/logion/model/loc_vos.js"; +import { SecretRecoveryRequestDescription } from "src/logion/model/secret_recovery.model.js"; +import moment from "moment"; export const notifiedProtection: ProtectionRequestDescription & { decision: LegalOfficerDecisionDescription } = { + id: "a7ff4ab6-5bef-4310-9c28-bcbd653565c3", + status: "PENDING", requesterAddress: ValidAccountId.polkadot("5H4MvAsobfZ6bBCDyj5dsrWYLrA8HrRzaqa9p61UXtxMhSCY"), requesterIdentityLocId: "7a6ca6b7-87ca-4e55-9c5f-422c9f799b74", legalOfficerAddress: ALICE_ACCOUNT, @@ -80,6 +84,28 @@ const vaultTransfer: VaultTransferRequestDescription = { }, }; +const secret: SecretRecoveryRequestDescription = { + id: "id", + challenge: "00475b3e-cf23-4fdc-a057-80372dc44f9e", + createdOn: moment("2021-06-10T16:25:23.668294"), + requesterIdentityLocId: "0f9ce42d-e020-4168-a5aa-e72618a8a882", + secretName: "Key", + status: "REJECTED", + userIdentity: { + firstName: "John", + lastName: "Doe", + email: "john@doe.com", + phoneNumber: "123465", + }, + userPostalAddress: { + line1: "Rue de la Paix", + line2: "", + postalCode: "4000", + city: "Liège", + country: "Belgium" + } +}; + export function notificationData() { const lo = notifiedLegalOfficer(ALICE_ACCOUNT.address); const otherLo = notifiedLegalOfficer(BOB_ACCOUNT.address); @@ -107,7 +133,13 @@ export function notificationData() { decisionOn: "2021-06-10T16:25:23.668294", rejectReason: "Failed to provide some data", } + }, + secret: { + ...secret, + decision: { + decisionOn: "2021-06-10T16:25:23.668294", + rejectReason: "Failed to provide some data", + } } } } -