From 7504c821cda4b9f2c93f7602a81e314ef148c37d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9rard=20Dethier?= Date: Wed, 22 May 2024 10:10:31 +0200 Subject: [PATCH] feat: enable secret recovery request review. logion-network/logion-internal#1264 --- src/common/AccountInfo.test.tsx | 4 - src/common/AccountInfo.tsx | 11 -- src/common/Spacer.css | 8 - .../__snapshots__/AccountInfo.test.tsx.snap | 129 -------------- src/legal-officer/LegalOfficerContext.tsx | 166 ++++++++++++++---- src/legal-officer/LegalOfficerPaths.tsx | 2 +- src/legal-officer/Model.tsx | 45 ++++- src/legal-officer/Types.ts | 20 ++- .../LegalOfficerRouter.test.tsx.snap | 2 +- .../recovery/PendingRecoveryRequests.tsx | 26 +-- .../recovery/RecoveryDetails.css | 4 + .../recovery/RecoveryDetails.test.tsx | 32 +++- .../recovery/RecoveryDetails.tsx | 102 +++++++---- .../recovery/RecoveryRequestDetails.tsx | 64 ------- .../recovery/RecoveryRequestsHistory.tsx | 14 +- .../RecoveryRequestsHistory.test.tsx.snap | 18 +- 16 files changed, 300 insertions(+), 347 deletions(-) delete mode 100644 src/legal-officer/recovery/RecoveryRequestDetails.tsx diff --git a/src/common/AccountInfo.test.tsx b/src/common/AccountInfo.test.tsx index 9e61a330..d156205e 100644 --- a/src/common/AccountInfo.test.tsx +++ b/src/common/AccountInfo.test.tsx @@ -3,7 +3,6 @@ import { UserIdentity as IdentityType, PostalAddress as PostalAddressType } from import { render } from "../tests"; import { DEFAULT_IDENTITY, COLOR_THEME, DEFAULT_ADDRESS } from "./TestData"; import AccountInfo from "./AccountInfo"; -import { TEST_WALLET_USER } from "../wallet-user/TestData"; const DIFFERENT_IDENTITY: IdentityType = { firstName: "John2", @@ -24,7 +23,6 @@ test("renders without comparison", () => { const tree = render( { const tree = render( { const tree = render( - props.address } - colors={ props.colors } - squeeze={ props.squeeze } - noComparison={ true } - /> -
-
- -
- - -
-
-
@@ -512,49 +469,6 @@ exports[`renders and compares with same data 1`] = `
-
-
- -
- - -
-
-
@@ -1020,49 +934,6 @@ exports[`renders without comparison 1`] = `
-
-
- -
- - -
-
-
diff --git a/src/legal-officer/LegalOfficerContext.tsx b/src/legal-officer/LegalOfficerContext.tsx index 2e04b42b..6bd091f0 100644 --- a/src/legal-officer/LegalOfficerContext.tsx +++ b/src/legal-officer/LegalOfficerContext.tsx @@ -6,6 +6,9 @@ import { LocsState, Votes, ProtectionRequest, + UserIdentity, + PostalAddress, + ProtectionRequestStatus, } from '@logion/client'; import { LocType, LegalOfficerData, ValidAccountId } from "@logion/node-api"; @@ -35,14 +38,118 @@ export interface MissingSettings { region: boolean; } +export interface BackendSecretRecoveryRequest { + id: string; + userIdentity: UserIdentity; + userPostalAddress: PostalAddress; + createdOn: string; + status: "PENDING" | "REJECTED" | "ACCEPTED"; + secretName: string; + +} + +export type RecoveryRequestStatus = ProtectionRequestStatus; + +export type RecoveryRequestType = "Account" | "Secret"; + +export abstract class RecoveryRequest { + + abstract get userIdentity(): UserIdentity; + + abstract get userPostalAddress(): PostalAddress; + + abstract get createdOn(): string; + + abstract get status(): RecoveryRequestStatus; + + abstract get type(): RecoveryRequestType; + + abstract get id(): string; +} + +export class AccountRecoveryRequest extends RecoveryRequest { + + constructor(request: ProtectionRequest) { + super(); + this.backendRequest = request; + } + + readonly backendRequest: ProtectionRequest; + + get userIdentity(): UserIdentity { + return this.backendRequest.userIdentity; + } + get userPostalAddress(): PostalAddress { + return this.backendRequest.userPostalAddress; + } + get createdOn(): string { + return this.backendRequest.createdOn; + } + get status(): ProtectionRequestStatus { + return this.backendRequest.status; + } + get type(): RecoveryRequestType { + return "Account"; + } + get id(): string { + return this.backendRequest.id; + } +} + +export class SecretRecoveryRequest extends RecoveryRequest { + + constructor(request: BackendSecretRecoveryRequest) { + super(); + this.backendRequest = request; + } + + readonly backendRequest: BackendSecretRecoveryRequest; + + get userIdentity(): UserIdentity { + return this.backendRequest.userIdentity; + } + get userPostalAddress(): PostalAddress { + return this.backendRequest.userPostalAddress; + } + get createdOn(): string { + return this.backendRequest.createdOn; + } + get status(): ProtectionRequestStatus { + return this.backendRequest.status; + } + get type(): RecoveryRequestType { + return "Secret"; + } + get id(): string { + return this.backendRequest.id; + } +} + +export const SECRET_RECOVERY_REQUEST = { + id: "secret-recovery-request-id", + createdOn: "2024-05-17T12:00:34.123", + secretName: "Key", + status: "PENDING", + userIdentity: { + email: "gerard@logion.network", + firstName: "GĂ©rard", + lastName: "Dethier", + phoneNumber: "+1234", + }, + userPostalAddress: { + city: "?", + country: "?", + line1: "?", + line2: "", + postalCode: "?", + }, +} as BackendSecretRecoveryRequest; + export interface LegalOfficerContext { - refreshRequests: ((clearBeforeRefresh: boolean) => void), - locsState: LocsState | null, - pendingProtectionRequests: ProtectionRequest[] | null, - activatedProtectionRequests: ProtectionRequest[] | null, - protectionRequestsHistory: ProtectionRequest[] | null, - pendingRecoveryRequests: ProtectionRequest[] | null, - recoveryRequestsHistory: ProtectionRequest[] | null, + refreshRequests: ((clearBeforeRefresh: boolean) => void); + locsState: LocsState | null; + pendingRecoveryRequests: RecoveryRequest[] | null; + recoveryRequestsHistory: RecoveryRequest[] | null; axios?: AxiosInstance; pendingVaultTransferRequests?: VaultTransferRequest[]; vaultTransferRequestsHistory?: VaultTransferRequest[]; @@ -76,9 +183,6 @@ function initialContextValue(): FullLegalOfficerContext { dataAddress: null, refreshRequests: DEFAULT_NOOP, locsState: null, - pendingProtectionRequests: null, - activatedProtectionRequests: null, - protectionRequestsHistory: null, pendingRecoveryRequests: null, recoveryRequestsHistory: null, updateSetting: () => Promise.reject(), @@ -134,11 +238,8 @@ interface Action { type: ActionType; dataAddress?: ValidAccountId; locsState?: LocsState; - pendingProtectionRequests?: ProtectionRequest[]; - protectionRequestsHistory?: ProtectionRequest[]; - activatedProtectionRequests?: ProtectionRequest[]; - pendingRecoveryRequests?: ProtectionRequest[], - recoveryRequestsHistory?: ProtectionRequest[], + pendingRecoveryRequests?: RecoveryRequest[], + recoveryRequestsHistory?: RecoveryRequest[], refreshRequests?: (clearBeforeRefresh: boolean) => void; clearBeforeRefresh?: boolean; axios?: AxiosInstance; @@ -203,9 +304,6 @@ const reducer: Reducer = (state: FullLegalOffic if(action.dataAddress === state.dataAddress) { return { ...state, - pendingProtectionRequests: action.pendingProtectionRequests!, - protectionRequestsHistory: action.protectionRequestsHistory!, - activatedProtectionRequests: action.activatedProtectionRequests!, pendingRecoveryRequests: action.pendingRecoveryRequests!, recoveryRequestsHistory: action.recoveryRequestsHistory!, pendingVaultTransferRequests: action.pendingVaultTransferRequests!, @@ -506,20 +604,25 @@ export function LegalOfficerContextProvider(props: Props) { (async function() { const axios = axiosFactory(currentAddress); - const allRequests = await fetchProtectionRequests(axios, { + const allAccountRecoveryRequests = await fetchProtectionRequests(axios, { legalOfficerAddress: currentAddress.address, }); - const pendingProtectionRequests = allRequests.filter(request => ["PENDING"].includes(request.status) && !request.isRecovery); - const activatedProtectionRequests = allRequests.filter(request => ["ACTIVATED"].includes(request.status)); - const pendingRecoveryRequests = allRequests.filter(request => ["PENDING"].includes(request.status) && request.isRecovery); - const protectionRequestsHistory = allRequests.filter(request => - ["ACCEPTED", "REJECTED", "ACTIVATED", "CANCELLED", "REJECTED_CANCELLED", "ACCEPTED_CANCELLED"].includes(request.status) - && !request.isRecovery - ); - const recoveryRequestsHistory = allRequests.filter(request => - ["ACCEPTED", "REJECTED", "ACTIVATED", "CANCELLED", "REJECTED_CANCELLED", "ACCEPTED_CANCELLED"].includes(request.status) - && request.isRecovery - ); + const pendingAccountRecoveryRequests: RecoveryRequest[] = allAccountRecoveryRequests + .filter(request => ["PENDING"].includes(request.status) && request.isRecovery) + .map(request => new AccountRecoveryRequest(request)); + const accountRecoveryRequestsHistory: RecoveryRequest[] = allAccountRecoveryRequests + .filter(request => + ["ACCEPTED", "REJECTED", "ACTIVATED", "CANCELLED", "REJECTED_CANCELLED", "ACCEPTED_CANCELLED"].includes(request.status) + && request.isRecovery + ) + .map(request => new AccountRecoveryRequest(request)); + const pendingSecretRecoveryRequests: RecoveryRequest[] = [SECRET_RECOVERY_REQUEST].map(request => new SecretRecoveryRequest(request)); + const pendingRecoveryRequests: RecoveryRequest[] = pendingAccountRecoveryRequests + .concat(pendingSecretRecoveryRequests) + .sort((a, b) => b.createdOn.localeCompare(a.createdOn)); + const recoveryRequestsHistory = accountRecoveryRequestsHistory + + .sort((a, b) => b.createdOn.localeCompare(a.createdOn)); const allVaultTransferRequestsResult = (await new VaultApi(axios, currentAddress).getVaultTransferRequests({ legalOfficerAddress: currentAddress.address, @@ -532,9 +635,6 @@ export function LegalOfficerContextProvider(props: Props) { dispatch({ type: "SET_REQUESTS_DATA", dataAddress: currentAddress, - pendingProtectionRequests, - activatedProtectionRequests, - protectionRequestsHistory, pendingRecoveryRequests, recoveryRequestsHistory, pendingVaultTransferRequests, diff --git a/src/legal-officer/LegalOfficerPaths.tsx b/src/legal-officer/LegalOfficerPaths.tsx index 829059b0..872dbcba 100644 --- a/src/legal-officer/LegalOfficerPaths.tsx +++ b/src/legal-officer/LegalOfficerPaths.tsx @@ -13,7 +13,7 @@ export const HOME_PATH = LEGAL_OFFICER_PATH; export const RECOVERY_REQUESTS_RELATIVE_PATH = '/recovery'; export const RECOVERY_REQUESTS_PATH = LEGAL_OFFICER_PATH + RECOVERY_REQUESTS_RELATIVE_PATH; -export const RECOVERY_DETAILS_RELATIVE_PATH = '/recovery-details/:requestId'; +export const RECOVERY_DETAILS_RELATIVE_PATH = RECOVERY_REQUESTS_RELATIVE_PATH + '/:requestId'; export const RECOVERY_DETAILS_PATH = LEGAL_OFFICER_PATH + RECOVERY_DETAILS_RELATIVE_PATH; export function recoveryDetailsPath(requestId: string): string { return RECOVERY_DETAILS_PATH.replace(":requestId", requestId); diff --git a/src/legal-officer/Model.tsx b/src/legal-officer/Model.tsx index 3bb930c8..d647667d 100644 --- a/src/legal-officer/Model.tsx +++ b/src/legal-officer/Model.tsx @@ -1,10 +1,51 @@ import { AxiosInstance } from 'axios'; import { RecoveryInfo } from './Types'; +import { SECRET_RECOVERY_REQUEST } from './LegalOfficerContext'; +import { ProtectionRequest } from '@logion/client'; + +export interface BackendRecoveryInfo { + addressToRecover: string, + recoveryAccount: ProtectionRequest, + accountToRecover?: ProtectionRequest, +} export async function fetchRecoveryInfo( axios: AxiosInstance, requestId: string ): Promise { - const response = await axios.put(`/api/protection-request/${requestId}/recovery-info`, {}) - return response.data; + if(requestId === SECRET_RECOVERY_REQUEST.id) { + return { + type: "Secret", + identity1: { + userIdentity: SECRET_RECOVERY_REQUEST.userIdentity, + postalAddress: SECRET_RECOVERY_REQUEST.userPostalAddress, + }, + identity2: { + userIdentity: SECRET_RECOVERY_REQUEST.userIdentity, + postalAddress: SECRET_RECOVERY_REQUEST.userPostalAddress, + }, + }; + } else { + const response = await axios.put(`/api/protection-request/${requestId}/recovery-info`) + const recoveryInfo = response.data as BackendRecoveryInfo; + let identity1 = undefined; + if(recoveryInfo.accountToRecover) { + identity1 = { + userIdentity: recoveryInfo.accountToRecover.userIdentity, + postalAddress: recoveryInfo.accountToRecover.userPostalAddress, + } + } + return { + type: "Account", + identity1, + identity2: { + userIdentity: recoveryInfo.recoveryAccount.userIdentity, + postalAddress: recoveryInfo.recoveryAccount.userPostalAddress, + }, + accountRecovery: { + address1: recoveryInfo.addressToRecover, + address2: recoveryInfo.recoveryAccount.requesterAddress, + } + }; + } } diff --git a/src/legal-officer/Types.ts b/src/legal-officer/Types.ts index 515275f4..da2ce787 100644 --- a/src/legal-officer/Types.ts +++ b/src/legal-officer/Types.ts @@ -1,11 +1,21 @@ -import { ProtectionRequest } from '@logion/client/dist/RecoveryClient.js'; - import { ColorTheme, rgbaToHex } from '../common/ColorTheme'; +import { PostalAddress, UserIdentity } from '@logion/client'; +import { RecoveryRequestType } from './LegalOfficerContext'; export interface RecoveryInfo { - addressToRecover: string, - recoveryAccount: ProtectionRequest, - accountToRecover?: ProtectionRequest, + type: RecoveryRequestType; + identity1?: { + userIdentity: UserIdentity; + postalAddress: PostalAddress; + }, + identity2: { + userIdentity: UserIdentity; + postalAddress: PostalAddress; + }, + accountRecovery?: { + address1: string; + address2: string; + }, } export const LIGHT_MODE: ColorTheme = { diff --git a/src/legal-officer/__snapshots__/LegalOfficerRouter.test.tsx.snap b/src/legal-officer/__snapshots__/LegalOfficerRouter.test.tsx.snap index f7f69b0c..ad0042a4 100644 --- a/src/legal-officer/__snapshots__/LegalOfficerRouter.test.tsx.snap +++ b/src/legal-officer/__snapshots__/LegalOfficerRouter.test.tsx.snap @@ -12,7 +12,7 @@ exports[`renders 1`] = ` /> } - path="/recovery-details/:requestId" + path="/recovery/:requestId" /> []; + let columns: Column[]; columns = [ { header: "First name", render: request => , - width: "200px", align: 'left', }, { header: "Last name", render: request => , - width: "200px", - renderDetails: request => , align: 'left', }, { header: "Status", render: request => , width: "140px", - splitAfter: true, }, { header: "Submission date", @@ -96,16 +91,9 @@ export default function PendingRecoveryRequests() { width: "120px", }, { - header: "Account number", - render: request => , - align: 'left', - }, - { - header: "Account to recover", - render: request => , - align: 'left', + header: "Type", + render: request => , + width: "200px", }, { header: "Action", @@ -119,6 +107,7 @@ export default function PendingRecoveryRequests() { ), + width: "300px", } ]; @@ -161,7 +150,6 @@ export default function PendingRecoveryRequests() { > .row:first-child { + position: relative; +} + .RecoveryDetails h3 { text-align: center; font-weight: bold; diff --git a/src/legal-officer/recovery/RecoveryDetails.test.tsx b/src/legal-officer/recovery/RecoveryDetails.test.tsx index f0cd74f7..32429fc9 100644 --- a/src/legal-officer/recovery/RecoveryDetails.test.tsx +++ b/src/legal-officer/recovery/RecoveryDetails.test.tsx @@ -30,9 +30,19 @@ describe("RecoveryDetails", () => { }); const protectionRequest = PROTECTION_REQUESTS_HISTORY[0]; const recoveryConfig: RecoveryInfo = { - addressToRecover: protectionRequest.addressToRecover!, - accountToRecover: protectionRequest, - recoveryAccount: protectionRequest, + type: "Account", + identity1: { + userIdentity: protectionRequest.userIdentity, + postalAddress: protectionRequest.userPostalAddress, + }, + identity2: { + userIdentity: protectionRequest.userIdentity, + postalAddress: protectionRequest.userPostalAddress, + }, + accountRecovery: { + address1: protectionRequest.addressToRecover!, + address2: protectionRequest.requesterAddress, + }, }; setFetchRecoveryInfo(jest.fn().mockResolvedValue(recoveryConfig)); setParams({ requestId: protectionRequest.id }); @@ -69,9 +79,19 @@ describe("RecoveryDetails", () => { }); const protectionRequest = PROTECTION_REQUESTS_HISTORY[0]; const recoveryConfig: RecoveryInfo = { - addressToRecover: protectionRequest.addressToRecover!, - accountToRecover: protectionRequest, - recoveryAccount: protectionRequest, + type: "Account", + identity1: { + userIdentity: protectionRequest.userIdentity, + postalAddress: protectionRequest.userPostalAddress, + }, + identity2: { + userIdentity: protectionRequest.userIdentity, + postalAddress: protectionRequest.userPostalAddress, + }, + accountRecovery: { + address1: protectionRequest.addressToRecover!, + address2: protectionRequest.requesterAddress, + }, }; setFetchRecoveryInfo(jest.fn().mockResolvedValue(recoveryConfig)); setParams({ requestId: protectionRequest.id }); diff --git a/src/legal-officer/recovery/RecoveryDetails.tsx b/src/legal-officer/recovery/RecoveryDetails.tsx index bad23bff..fea3cf96 100644 --- a/src/legal-officer/recovery/RecoveryDetails.tsx +++ b/src/legal-officer/recovery/RecoveryDetails.tsx @@ -53,15 +53,13 @@ export default function RecoveryDetails() { lost, rescuer ); - return !!(activeRecovery && activeRecovery.legalOfficers.find(lo => lo.equals(currentAddress))); - }, [ api ]); - const accept = useCallback(async () => { + const acceptAccountRecovery = useCallback(async () => { const currentAddress = accounts!.current!.accountId; - const lost = ValidAccountId.polkadot(recoveryInfo!.accountToRecover!.requesterAddress); - const rescuer = ValidAccountId.polkadot(recoveryInfo!.recoveryAccount.requesterAddress); + const lost = ValidAccountId.polkadot(recoveryInfo!.accountRecovery!.address1); + const rescuer = ValidAccountId.polkadot(recoveryInfo!.accountRecovery!.address2); if (await alreadyVouched(lost, rescuer, currentAddress)) { await acceptProtectionRequest(axiosFactory!(currentAddress)!, { @@ -116,7 +114,7 @@ export default function RecoveryDetails() { return ( - - - I did my due diligence and authorize the transfer of all assets
- from the account address "From" to the account address "To" as detailed below : -
-
-

From

+

Identity 1

-

To

+

Identity 2

{ - (extrinsicSubmissionState.canSubmit() || extrinsicSubmissionState.callEnded) && + recoveryInfo.type === "Account" && - - - + + I did my due diligence and authorize the transfer of all assets + from the account address { recoveryInfo.accountRecovery?.address1 || "" }{" "} + to the account address { recoveryInfo.accountRecovery?.address1 || "" } as detailed below : + + + } + { + recoveryInfo.type === "Secret" && + + + I did my due diligence and authorize the retrieval of a secret by the above person. + + + } + + + + + { + recoveryInfo.type === "Account" && - - - } + } + { + recoveryInfo.type === "Secret" && + + } + + - I did my due diligence and refuse to grant the - account { recoveryInfo.accountToRecover?.requesterAddress || "-" } the right to transfer all assets - to the account { recoveryInfo.recoveryAccount.requesterAddress }. + { + recoveryInfo.type === "Account" && + <> + I did my due diligence and refuse to grant the + account { recoveryInfo.accountRecovery?.address1 || "-" } the right to transfer all assets + to the account { recoveryInfo.accountRecovery?.address2 || "-" }. + + } + { + recoveryInfo.type === "Secret" && + <> + I did my due diligence and do not authorize the retrieval of a secret by the above person. + + } Reason - - - - - - - - - - - - - - - - - - - - ); -} diff --git a/src/legal-officer/recovery/RecoveryRequestsHistory.tsx b/src/legal-officer/recovery/RecoveryRequestsHistory.tsx index d1979637..f9b6ffb1 100644 --- a/src/legal-officer/recovery/RecoveryRequestsHistory.tsx +++ b/src/legal-officer/recovery/RecoveryRequestsHistory.tsx @@ -1,8 +1,7 @@ -import Table, { Cell, EmptyTableMessage, DateTimeCell, CopyPasteCell } from '../../common/Table'; +import Table, { Cell, EmptyTableMessage, DateTimeCell } from '../../common/Table'; import { useLegalOfficerContext } from '../LegalOfficerContext'; import RecoveryRequestStatus from './RecoveryRequestStatus'; -import RecoveryRequestDetails from './RecoveryRequestDetails'; export default function RecoveryRequestsHistory() { @@ -26,7 +25,6 @@ export default function RecoveryRequestsHistory() { header: "Last name", render: request => , width: "200px", - renderDetails: request => , align: 'left', }, { @@ -42,15 +40,9 @@ export default function RecoveryRequestsHistory() { smallerText: true, }, { - header: "Account number", - render: request => , - align: 'left', + header: "Type", + render: request => , }, - { - header: "Account to recover", - render: request => , - align: 'left', - } ]} data={ recoveryRequestsHistory } renderEmpty={ () => No processed request} diff --git a/src/legal-officer/recovery/__snapshots__/RecoveryRequestsHistory.test.tsx.snap b/src/legal-officer/recovery/__snapshots__/RecoveryRequestsHistory.test.tsx.snap index d968da87..dce296c6 100644 --- a/src/legal-officer/recovery/__snapshots__/RecoveryRequestsHistory.test.tsx.snap +++ b/src/legal-officer/recovery/__snapshots__/RecoveryRequestsHistory.test.tsx.snap @@ -15,7 +15,6 @@ exports[`Renders null with no data 1`] = ` "align": "left", "header": "Last name", "render": [Function], - "renderDetails": [Function], "width": "200px", }, Object { @@ -31,13 +30,7 @@ exports[`Renders null with no data 1`] = ` "width": "120px", }, Object { - "align": "left", - "header": "Account number", - "render": [Function], - }, - Object { - "align": "left", - "header": "Account to recover", + "header": "Type", "render": [Function], }, ] @@ -62,7 +55,6 @@ exports[`Renders requests history 1`] = ` "align": "left", "header": "Last name", "render": [Function], - "renderDetails": [Function], "width": "200px", }, Object { @@ -78,13 +70,7 @@ exports[`Renders requests history 1`] = ` "width": "120px", }, Object { - "align": "left", - "header": "Account number", - "render": [Function], - }, - Object { - "align": "left", - "header": "Account to recover", + "header": "Type", "render": [Function], }, ]