Skip to content

Commit

Permalink
refactor: service cleanup
Browse files Browse the repository at this point in the history
  • Loading branch information
wkolod committed Aug 9, 2023
1 parent 24c10da commit 454cd8f
Show file tree
Hide file tree
Showing 6 changed files with 257 additions and 237 deletions.
59 changes: 59 additions & 0 deletions src/core/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { ListOptions } from "../types/query-options";
import lodash from "lodash";
import { EncryptionMetadata } from "./service";
import { EncryptedPayload } from "@akord/crypto/lib/types";
import { base64ToArray } from "@akord/crypto";

export const handleListErrors = async <T>(originalItems: Array<T>, promises: Array<Promise<T>>)
: Promise<{ items: Array<T>, errors: Array<{ id: string, error: Error }> }> => {
const results = await Promise.all(promises.map(p => p.catch(e => e)));
const items = results.filter(result => !(result instanceof Error));
const errors = results
.map((result, index) => ({ result, index }))
.filter((mapped) => mapped.result instanceof Error)
.map((filtered) => ({ id: (<any>originalItems[filtered.index]).id, error: filtered.result }));
return { items, errors };
}

export const paginate = async <T>(apiCall: any, listOptions: ListOptions & { vaultId?: string }): Promise<Array<T>> => {
let token = undefined;
let results = [] as T[];
do {
const { items, nextToken } = await apiCall(listOptions);
results = results.concat(items);
token = nextToken;
listOptions.nextToken = nextToken;
if (nextToken === "null") {
token = undefined;
}
} while (token);
return results;
}

export const mergeState = (currentState: any, stateUpdates: any): any => {
let newState = lodash.cloneDeepWith(currentState);
lodash.mergeWith(
newState,
stateUpdates,
function concatArrays(objValue, srcValue) {
if (lodash.isArray(objValue)) {
return objValue.concat(srcValue);
}
});
return newState;
}

export const getEncryptedPayload = (data: ArrayBuffer | string, metadata: EncryptionMetadata)
: EncryptedPayload => {
const { encryptedKey, iv } = metadata;
if (encryptedKey && iv) {
return {
encryptedKey,
encryptedData: {
iv: base64ToArray(iv),
ciphertext: data as ArrayBuffer
}
}
}
return null;
}
54 changes: 50 additions & 4 deletions src/core/membership.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import { actionRefs, objectType, status, functions, protocolTags } from "../constants";
import { v4 as uuidv4 } from "uuid";
import { EncryptedKeys, Encrypter, deriveAddress, base64ToArray } from "@akord/crypto";
import { EncryptedKeys, Encrypter, deriveAddress, base64ToArray, generateKeyPair, Keys } from "@akord/crypto";
import { Service } from "./service";
import { Membership, RoleType, StatusType } from "../types/membership";
import { GetOptions, ListOptions } from "../types/query-options";
import { MembershipInput, Tag, Tags } from "../types/contract";
import { Paginated } from "../types/paginated";
import { BadRequest } from "../errors/bad-request";
import { IncorrectEncryptionKey } from "../errors/incorrect-encryption-key";
import { handleListErrors, paginate } from "./common";
import { ProfileService } from "./profile";
import { ProfileDetails } from "../types/profile-details";

export const activeStatus = [status.ACCEPTED, status.PENDING, status.INVITED] as StatusType[];

Expand Down Expand Up @@ -60,7 +63,7 @@ class MembershipService extends Service {
.map(async (membershipProto: Membership) => {
return await this.processMembership(membershipProto, !membershipProto.__public__ && listOptions.shouldDecrypt, membershipProto.__keys__);
}) as Promise<Membership>[];
const { items, errors } = await this.handleListErrors<Membership>(response.items, promises);
const { items, errors } = await handleListErrors<Membership>(response.items, promises);
return {
items,
nextToken: response.nextToken,
Expand All @@ -77,7 +80,7 @@ class MembershipService extends Service {
const list = async (options: ListOptions & { vaultId: string }) => {
return await this.list(options.vaultId, options);
}
return await this.paginate<Membership>(list, { ...options, vaultId });
return await paginate<Membership>(list, { ...options, vaultId });
}

/**
Expand Down Expand Up @@ -196,7 +199,8 @@ class MembershipService extends Service {
* @returns Promise with corresponding transaction id
*/
public async accept(membershipId: string): Promise<MembershipUpdateResult> {
const memberDetails = await this.getProfileDetails();
const profileService = new ProfileService(this.wallet, this.api);
const memberDetails = await profileService.get();
const service = new MembershipService(this.wallet, this.api);
await service.setVaultContextFromMembershipId(membershipId);
const state = {
Expand Down Expand Up @@ -457,6 +461,48 @@ class MembershipService extends Service {
return null;
}
}

async rotateMemberKeys(publicKeys: Map<string, string>): Promise<{
memberKeys: Map<string, EncryptedKeys[]>,
keyPair: Keys
}> {
const memberKeys = new Map<string, EncryptedKeys[]>();
// generate a new vault key pair
const keyPair = await generateKeyPair();

for (let [memberId, publicKey] of publicKeys) {
const memberKeysEncrypter = new Encrypter(
this.wallet,
this.dataEncrypter.keys,
base64ToArray(publicKey)
);
try {
memberKeys.set(memberId, [await memberKeysEncrypter.encryptMemberKey(keyPair)]);
} catch (error) {
throw new IncorrectEncryptionKey(error);
}
}
return { memberKeys, keyPair };
}

async processMemberDetails(memberDetails: { name?: string, avatar?: ArrayBuffer }, cacheOnly?: boolean) {
const processedMemberDetails = {} as ProfileDetails;
if (!this.isPublic) {
if (memberDetails.name) {
processedMemberDetails.name = await this.processWriteString(memberDetails.name);
}
if (memberDetails.avatar) {
const { resourceUrl, resourceTx } = await this.processAvatar(memberDetails.avatar, cacheOnly);
processedMemberDetails.avatarUri = [`arweave:${resourceTx}`, `s3:${resourceUrl}`];
}
}
return new ProfileDetails(processedMemberDetails);
}

private async processAvatar(avatar: ArrayBuffer, cacheOnly?: boolean): Promise<{ resourceTx: string, resourceUrl: string }> {
const { processedData, encryptionTags } = await this.processWriteRaw(avatar);
return this.api.uploadFile(processedData, encryptionTags, { cacheOnly, public: false });
}
};

export type MembershipCreateOptions = {
Expand Down
5 changes: 3 additions & 2 deletions src/core/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { Paginated } from '../types/paginated';
import { v4 as uuidv4 } from "uuid";
import { IncorrectEncryptionKey } from '../errors/incorrect-encryption-key';
import { BadRequest } from '../errors/bad-request';
import { handleListErrors, paginate } from './common';

class NodeService<T> extends Service {
objectType: NodeType;
Expand Down Expand Up @@ -64,7 +65,7 @@ class NodeService<T> extends Service {
.map(async (nodeProto: any) => {
return await this.processNode(nodeProto, !nodeProto.__public__ && listOptions.shouldDecrypt, nodeProto.__keys__);
});
const { items, errors } = await this.handleListErrors<T>(response.items, promises);
const { items, errors } = await handleListErrors<T>(response.items, promises);
return {
items,
nextToken: response.nextToken,
Expand All @@ -81,7 +82,7 @@ class NodeService<T> extends Service {
const list = async (options: ListOptions & { vaultId: string }) => {
return await this.list(options.vaultId, options);
}
return await this.paginate<T>(list, { ...options, vaultId });
return await paginate<T>(list, { ...options, vaultId });
}

/**
Expand Down
65 changes: 58 additions & 7 deletions src/core/profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import { Service } from "./service";
import { objectType } from "../constants";
import { Membership } from "../types/membership";
import { ListOptions } from "../types/query-options";
import { getEncryptedPayload, handleListErrors, paginate } from "./common";
import { Encrypter, arrayToString } from "@akord/crypto";
import { IncorrectEncryptionKey } from "../errors/incorrect-encryption-key";

class ProfileService extends Service {
objectType = objectType.PROFILE;
Expand All @@ -20,7 +23,42 @@ class ProfileService extends Service {
shouldCacheDecider: () => CacheBusters.cache
})
public async get(): Promise<ProfileDetails> {
return await this.getProfileDetails();
const user = await this.api.getUser();
if (user) {
const profileEncrypter = new Encrypter(this.wallet, null, null);
profileEncrypter.decryptedKeys = [
{
publicKey: this.wallet.publicKeyRaw(),
privateKey: this.wallet.privateKeyRaw()
}
]
let avatar = null;
const resourceUri = getAvatarUri(new ProfileDetails(user));
if (resourceUri) {
const { fileData, metadata } = await this.api.downloadFile(resourceUri);
const encryptedPayload = getEncryptedPayload(fileData, metadata);
try {
if (encryptedPayload) {
avatar = await profileEncrypter.decryptRaw(encryptedPayload, false);
} else {
const dataString = arrayToString(new Uint8Array(fileData));
avatar = await profileEncrypter.decryptRaw(dataString, true);
}
} catch (error) {
throw new IncorrectEncryptionKey(error);
}
}
try {
const decryptedProfile = await profileEncrypter.decryptObject(
user,
['name'],
);
return { ...decryptedProfile, avatar }
} catch (error) {
throw new IncorrectEncryptionKey(error);
}
}
return <any>{};
}

/**
Expand All @@ -38,11 +76,12 @@ class ProfileService extends Service {
}> {
// update profile
const user = await this.api.getUser();
this.setObject(<any>user);

this.setRawDataEncryptionPublicKey(this.wallet.publicKeyRaw());
this.setIsPublic(false);
const profileDetails = await this.processMemberDetails({ name, avatar }, true);
const service = new MembershipService(this.wallet, this.api);

service.setRawDataEncryptionPublicKey(this.wallet.publicKeyRaw());
service.setIsPublic(false);
const profileDetails = await service.processMemberDetails({ name, avatar }, true);

const newProfileDetails = new ProfileDetails({
...user,
Expand All @@ -61,18 +100,30 @@ class ProfileService extends Service {
transactions.push({ id: membership.id, transactionId: transactionId });
return membership;
})
const { errors } = await this.handleListErrors(memberships, membershipPromises);
const { errors } = await handleListErrors(memberships, membershipPromises);
return { transactions, errors };
}

private async listMemberships(): Promise<Array<Membership>> {
const list = async (listOptions: ListOptions) => {
return await this.api.getMemberships(listOptions);
}
return await this.paginate<Membership>(list, {});
return await paginate<Membership>(list, {});
}
};

const getAvatarUri = (profileDetails: ProfileDetails) => {
if (profileDetails.avatarUri && profileDetails.avatarUri.length) {
const avatarUri = [...profileDetails.avatarUri]
.reverse()
.find(resourceUri => resourceUri.startsWith("s3:"))
?.replace("s3:", "");
return avatarUri !== "null" && avatarUri;
} else {
return null;
}
}

export {
ProfileService
}
Loading

0 comments on commit 454cd8f

Please sign in to comment.