Skip to content

Commit

Permalink
feat: enable secret recovery request.
Browse files Browse the repository at this point in the history
  • Loading branch information
benoitdevos committed May 21, 2024
1 parent cea09c3 commit 4592751
Show file tree
Hide file tree
Showing 8 changed files with 186 additions and 18 deletions.
51 changes: 44 additions & 7 deletions packages/client-node/integration/Secrets.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,67 @@
import { ClosedIdentityLoc } from "@logion/client";
import { ClosedIdentityLoc, CreateSecretRecoveryRequest } from "@logion/client";
import { State } from "./Utils";
import { UUID } from "@logion/node-api";

const secretToKeep = "Key";

export async function recoverableSecrets(state: State) {
const requesterIdentityLocId = await addSecrets(state);
await createRecoveryRequest(state, requesterIdentityLocId);
}

async function addSecrets(state: State): Promise<UUID> {
const { requesterAccount } = state;

const client = state.client.withCurrentAccount(requesterAccount);
let locsState = await client.locsState();

let closedIdentityLoc = locsState.closedLocs.Identity[0] as ClosedIdentityLoc;
expect(closedIdentityLoc).toBeInstanceOf(ClosedIdentityLoc);
const name = "Key";
const value = "Encrypted key";

closedIdentityLoc = await closedIdentityLoc.addSecret({
name,
name: secretToKeep,
value,
});
const secretToRemove = "secret-to-remove";
closedIdentityLoc = await closedIdentityLoc.addSecret({
name: secretToRemove,
value,
});

expect(closedIdentityLoc).toBeInstanceOf(ClosedIdentityLoc);

let data = closedIdentityLoc.data();
expect(data.secrets.length).toBe(2);

closedIdentityLoc = await closedIdentityLoc.removeSecret(secretToRemove);

data = closedIdentityLoc.data();
expect(data.secrets.length).toBe(1);
expect(data.secrets[0].name).toBe(name);
expect(data.secrets[0].name).toBe(secretToKeep);
expect(data.secrets[0].value).toBe(value);

closedIdentityLoc = await closedIdentityLoc.removeSecret(name);
return closedIdentityLoc.locId;
}

data = closedIdentityLoc.data();
expect(data.secrets.length).toBe(0);
async function createRecoveryRequest(state: State, requesterIdentityLocId: UUID) {
const request: CreateSecretRecoveryRequest = {
requesterIdentityLocId,
secretName: secretToKeep,
challenge: "my-personal-challenge",
userIdentity: {
email: "[email protected]",
firstName: "John",
lastName: "Doe",
phoneNumber: "+1234",
},
userPostalAddress: {
line1: "Line1",
line2: "Line2",
postalCode: "PostalCode",
city: "City",
country: "Country",
}
}
await state.client.secretRecovery.createSecretRecoveryRequest(request);
}
2 changes: 1 addition & 1 deletion packages/client/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@logion/client",
"version": "0.45.0-7",
"version": "0.45.0-8",
"description": "logion SDK for client applications",
"main": "dist/index.js",
"packageManager": "[email protected]",
Expand Down
25 changes: 17 additions & 8 deletions packages/client/src/LogionClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,12 @@ import { VoterApi } from "./Voter.js";
import { SponsorshipState, SponsorshipApi } from "./Sponsorship.js";
import { requireDefined } from "./assertions.js";
import { InvitedContributorApi } from "./InvitedContributor.js";
import { SecretRecoveryApi } from "./SecretRecovery.js";

/**
* An instance of LogionClient is connected to a Logion network and
* interacts with all its components (including the blockchain).
*
*
* It features:
* - Access to LGNT balance, transactions and transfer
* - LOC management
Expand All @@ -35,7 +36,7 @@ export class LogionClient {

/**
* Instantiates a connected client.
*
*
* @param config Parameters of a connection to the Logion network.
* @returns A connected client.
*/
Expand Down Expand Up @@ -71,6 +72,7 @@ export class LogionClient {
this._public = new PublicApi({ sharedState });
this._voter = new VoterApi({ sharedState, logionClient: this });
this._invitedContributor = new InvitedContributorApi({ sharedState, logionClient: this })
this._secretRecovery = new SecretRecoveryApi({ sharedState });
}

private sharedState: SharedState;
Expand All @@ -81,6 +83,8 @@ export class LogionClient {

private readonly _invitedContributor: InvitedContributorApi;

private readonly _secretRecovery: SecretRecoveryApi;

/**
* The configuration of this client.
*/
Expand Down Expand Up @@ -143,7 +147,7 @@ export class LogionClient {

/**
* Overrides current tokens.
*
*
* @param tokens The new tokens.
* @returns A copy of this client, but using the new tokens.
*/
Expand All @@ -165,7 +169,7 @@ export class LogionClient {

/**
* Postpones the expiration of valid (see {@link isTokenValid}) JWT tokens.
*
*
* @param now Current time, used to check if tokens are still valid or not.
* @param threshold If at least one token's expiration falls between now and (now + threshold), then tokens are refreshed. Otherwise, they are not.
* @returns An authenticated client using refreshed tokens or this if no refresh occured.
Expand Down Expand Up @@ -206,7 +210,7 @@ export class LogionClient {

/**
* Sets current account.
*
*
* @param currentAccount The account to use as current.
* @returns A client instance with provided current account.
*/
Expand Down Expand Up @@ -295,11 +299,11 @@ export class LogionClient {
* a JWT token for each address. A valid JWT token is sent back by a Logion node
* if the client was able to sign a random challenge, hence proving that provided
* signer is indeed able to sign using provided addresses.
*
*
* Note that the signer may be able to sign for more addresses than the once provided.
* A call to this method will merge the retrieved tokens with the ones already available.
* Older tokens are replaced.
*
*
* @param accounts The addresses for which an authentication token must be retrieved.
* @param signer The signer that will sign the challenge.
* @returns An instance of client with retrived JWT tokens.
Expand Down Expand Up @@ -503,9 +507,14 @@ export class LogionClient {
return this._invitedContributor;
}

get secretRecovery(): SecretRecoveryApi {
this.ensureConnected();
return this._secretRecovery;
}

/**
* Disconnects the client from the Logion blockchain.
* @returns
* @returns
*/
async disconnect() {
if(this.sharedState.nodeApi.polkadot.isConnected) {
Expand Down
15 changes: 15 additions & 0 deletions packages/client/src/SecretRecovery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { SharedState } from "./SharedClient.js";
import { CreateSecretRecoveryRequest, SecretRecoveryClient } from "./SecretRecoveryClient.js";

export class SecretRecoveryApi {

constructor(args: { sharedState: SharedState }) {
this.client = new SecretRecoveryClient(args);
}

private readonly client: SecretRecoveryClient;

async createSecretRecoveryRequest(params: CreateSecretRecoveryRequest): Promise<void> {
await this.client.createSecretRecoveryRequest(params);
}
}
49 changes: 49 additions & 0 deletions packages/client/src/SecretRecoveryClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { UserIdentity, PostalAddress } from "./Types";
import { LocMultiClient, FetchParameters } from "./LocClient.js";
import { requireDefined } from "./assertions.js";
import { SharedState } from "./SharedClient.js";
import { AxiosInstance } from "axios";
import { UUID } from "@logion/node-api";
import { newBackendError } from "./Error.js";

export interface CreateSecretRecoveryRequest {
requesterIdentityLocId: UUID;
secretName: string;
challenge: string;
userIdentity: UserIdentity;
userPostalAddress: PostalAddress;
}

export class SecretRecoveryClient {

constructor(args: { sharedState: SharedState }) {
this.sharedState = args.sharedState;
}

private sharedState: SharedState;

async createSecretRecoveryRequest(params: CreateSecretRecoveryRequest): Promise<void> {
const { secretName, challenge, userIdentity, userPostalAddress, requesterIdentityLocId } = params;
const axios = await this.backend({ locId: requesterIdentityLocId });
try {
await axios.post("/api/secret-recovery", {
requesterIdentityLocId: requesterIdentityLocId.toString(),
secretName,
challenge,
userIdentity,
userPostalAddress,
});
} catch (e) {
throw newBackendError(e);
}
}

private async backend(params: FetchParameters): Promise<AxiosInstance> {
const loc = await LocMultiClient.getLoc({
...params,
api: this.sharedState.nodeApi,
});
const legalOfficer = requireDefined(this.sharedState.legalOfficers.find(lo => lo.account.equals(loc.owner)));
return legalOfficer.buildAxiosToNode();
}
}
4 changes: 3 additions & 1 deletion packages/client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* Exposes tools enabling interaction with a Logion network.
* An instance of {@link LogionClient} must be created.
* Most features of the client require authentication.
*
*
* @module
*/
import { ISubmittableResult } from '@polkadot/types/types';
Expand Down Expand Up @@ -33,6 +33,8 @@ export * from './Polling.js';
export * from './Public.js';
export * from './Recovery.js';
export { LegalOfficerDecision, LoRecoveryClient, ProtectionRequest, ProtectionRequestStatus, UpdateParameters, UserActionParameters, CreateProtectionRequest } from './RecoveryClient.js';
export * from './SecretRecovery.js';
export * from './SecretRecoveryClient.js';
export * from './SharedClient.js';
export * from './Signer.js';
export * from './State.js';
Expand Down
57 changes: 57 additions & 0 deletions packages/client/test/SecretRecovery.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { LogionClient, LegalOfficerClass, PostalAddress, UserIdentity, } from "../src/index.js";
import { buildTestConfig, LOGION_CLIENT_CONFIG, ALICE } from "./Utils.js";
import { buildLocAndRequest } from "./LocUtils.js";
import { UUID } from "@logion/node-api";
import { Mock, It, Times } from "moq.ts";
import { AxiosInstance, AxiosResponse } from "axios";

describe("SecretRecovery", () => {

it("requests a Secret recovery", async () => {

const identityLoc = buildLocAndRequest(ALICE.account, "CLOSED", "Identity");
const locId = new UUID(identityLoc.request.id);
const axios = new Mock<AxiosInstance>();

const config = buildTestConfig(testConfigFactory => {
testConfigFactory.setupDefaultNetworkState();
const axiosFactory = testConfigFactory.setupAxiosFactoryMock();
axiosFactory.setup(instance => instance.buildAxiosInstance(It.IsAny<string>(), It.IsAny()))
.returns(axios.object());
setupBackend(axios, locId);
const nodeApi = testConfigFactory.setupNodeApiMock(LOGION_CLIENT_CONFIG);
const directoryClient = testConfigFactory.setupDirectoryClientMock(LOGION_CLIENT_CONFIG);

directoryClient.setup(instance => instance.getLegalOfficers()).returns(Promise.resolve([
new LegalOfficerClass({
legalOfficer: ALICE,
axiosFactory: axiosFactory.object(),
})
]));

nodeApi.setup(instance => instance.queries.getLegalOfficerCase(locId))
.returns(Promise.resolve(identityLoc.loc));
})

const client = await LogionClient.create(config);
await client.secretRecovery.createSecretRecoveryRequest({
requesterIdentityLocId: locId,
secretName: "secret-name",
challenge: "my-personal-challenge",
userIdentity: {} as UserIdentity,
userPostalAddress: {} as PostalAddress,
})
axios.verify(instance => instance.post("/api/secret-recovery", It.IsAny()), Times.Once());
})
})

function setupBackend(axios: Mock<AxiosInstance>, locId: UUID) {
const response = new Mock<AxiosResponse<any>>();
axios.setup(instance => instance.post("/api/secret-recovery", It.Is<{ requesterIdentityLocId: string, secretName: string, challenge: string }>(body =>
body.requesterIdentityLocId === locId.toString() &&
body.secretName === "secret-name" &&
body.challenge === "my-personal-challenge",
)))
.returns(Promise.resolve(response.object()))
}

1 change: 0 additions & 1 deletion packages/client/test/Voter.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import {
ALICE,
buildSimpleNodeApi,
buildTestAuthenticatedSharedSate,
buildValidPolkadotAccountId,
ItIsUuid,
LEGAL_OFFICERS,
LOGION_CLIENT_CONFIG,
Expand Down

0 comments on commit 4592751

Please sign in to comment.