Browse Source

Refactoring for cleanup

kb/add-usage-guide
Matthew Raymer 1 year ago
parent
commit
31d13b9143
  1. 70
      sample.txt
  2. 43
      src/db/tables/accounts.ts
  3. 41
      src/libs/endorserServer.ts
  4. 434
      src/views/AccountViewView.vue

70
sample.txt

@ -0,0 +1,70 @@
> kickstart-for-time-pwa@0.1.0 build
> vue-cli-service build
All browser targets in the browserslist configuration have supported ES module.
Therefore we don't build two separate bundles for differential loading.
ERROR Failed to compile with 6 errors8:44:37 PM
error in ./src/views/AccountViewView.vue?vue&type=script&lang=ts
Module not found: Error: Package path ./index is not exported from package /home/matthew/projects/kick-starter-for-time-pwa/node_modules/axios (see exports field in /home/matthew/projects/kick-starter-for-time-pwa/node_modules/axios/package.json)
error in src/libs/endorserServer.ts:226:53
TS18047: 'error' is possibly 'null'.
224 | error instanceof Error
225 | ? error.message
> 226 | : typeof error === "object" && "message" in error
| ^^^^^
227 | ? (error as { message: string }).message
228 | : "Unknown error";
229 |
error in src/views/AccountViewView.vue:474:29
TS2304: Cannot find name 'IdentityType'.
472 | * @param {IdentityType} identity - Object containing identity information.
473 | */
> 474 | processIdentity(identity: IdentityType) {
| ^^^^^^^^^^^^
475 | this.publicHex = identity.keys[0].publicKeyHex;
476 | this.publicBase64 = Buffer.from(this.publicHex, "hex").toString("base64");
477 | this.derivationPath = identity.keys[0].meta.derivationPath;
error in src/views/AccountViewView.vue:491:7
TS18046: 'err' is of type 'unknown'.
489 | handleError(err: unknown) {
490 | if (
> 491 | err.message ===
| ^^^
492 | "Attempted to load account records with no identity available."
493 | ) {
494 | this.limitsMessage = "No identity.";
error in src/views/AccountViewView.vue:645:34
TS2345: Argument of type 'unknown' is not assignable to parameter of type 'Error | AxiosError<unknown, any>'.
643 | }
644 | } catch (error) {
> 645 | this.handleRateLimitsError(error);
| ^^^^^
646 | }
647 |
648 | this.loadingLimits = false;
error in src/views/AccountViewView.vue:726:40
TS2345: Argument of type 'Account' is not assignable to parameter of type 'IAccount'.
Property 'privateHex' is missing in type 'Account' but required in type 'IAccount'.
724 | await db.settings.update(MASTER_SETTINGS_KEY, { activeDid: account.did });
725 |
> 726 | this.updateActiveAccountProperties(account);
| ^^^^^^^
727 | }
728 |
729 | /**

43
src/db/tables/accounts.ts

@ -1,17 +1,50 @@
/**
* Represents an account stored in the database.
*/
export type Account = {
id?: number; // auto-generated by Dexie
/**
* Auto-generated ID by Dexie.
*/
id?: number;
/**
* The date the account was created.
*/
dateCreated: string;
/**
* The derivation path for the account.
*/
derivationPath: string;
/**
* Decentralized Identifier (DID) for the account.
*/
did: string;
// stringified JSON containing underlying key material of type IIdentifier
// https://github.com/uport-project/veramo/blob/next/packages/core-types/src/types/IIdentifier.ts
/**
* Stringified JSON containing underlying key material.
* Based on the IIdentifier type from Veramo.
* @see {@link https://github.com/uport-project/veramo/blob/next/packages/core-types/src/types/IIdentifier.ts}
*/
identity: string;
/**
* The public key in hexadecimal format.
*/
publicKeyHex: string;
/**
* The mnemonic passphrase for the account.
*/
mnemonic: string;
};
// mark encrypted field by starting with a $ character
// see https://github.com/PVermeer/dexie-addon-suite-monorepo/tree/master/packages/dexie-encrypted-addon
/**
* Schema for the accounts table in the database.
* Fields starting with a $ character are encrypted.
* @see {@link https://github.com/PVermeer/dexie-addon-suite-monorepo/tree/master/packages/dexie-encrypted-addon}
*/
export const AccountsSchema = {
accounts:
"++id, dateCreated, derivationPath, did, $identity, $mnemonic, publicKeyHex",

41
src/libs/endorserServer.ts

@ -112,15 +112,23 @@ export function isHiddenDid(did: string) {
/**
always returns text, maybe UNNAMED_VISIBLE or UNKNOWN_ENTITY
**/
export function didInfo(did: string, activeDid: string, allMyDids: string[], contacts: Contact[]): string {
export function didInfo(
did: string,
activeDid: string,
allMyDids: string[],
contacts: Contact[],
): string {
const myId = R.find(R.equals(did), allMyDids);
if (myId) return `You${myId !== activeDid ? " (Alt ID)" : ""}`;
const contact = R.find(c => c.did === did, contacts);
return contact ? contact.name || "Someone Unnamed in Contacts" :
!did ? "Unspecified Person" :
isHiddenDid(did) ? "Someone Not In Network" :
"Someone Not In Contacts";
const contact = R.find((c) => c.did === did, contacts);
return contact
? contact.name || "Someone Unnamed in Contacts"
: !did
? "Unspecified Person"
: isHiddenDid(did)
? "Someone Not In Network"
: "Someone Not In Contacts";
}
export interface ResultWithType {
@ -166,7 +174,9 @@ export async function createAndSubmitGive(
agent: fromDid ? { identifier: fromDid } : undefined,
description: description || undefined,
object: hours ? { amountOfThisGood: hours, unitCode: "HUR" } : undefined,
fulfills: fulfillsProjectHandleId ? { "@type": "PlanAction", identifier: fulfillsProjectHandleId } : undefined,
fulfills: fulfillsProjectHandleId
? { "@type": "PlanAction", identifier: fulfillsProjectHandleId }
: undefined,
};
const vcPayload = {
@ -209,20 +219,21 @@ export async function createAndSubmitGive(
});
return { type: "success", response };
} catch (error: unknown) {
const errorMessage: string =
error instanceof Error ? error.message :
(typeof error === "object" && error?.message) ? error.message :
"Unknown error";
const errorMessage: string =
error instanceof Error
? error.message
: typeof error === "object" && "message" in error
? (error as { message: string }).message
: "Unknown error";
return {
type: "error",
error: {
error: errorMessage,
userMessage: "Failed to create and submit the claim."
}
};
userMessage: "Failed to create and submit the claim.",
},
};
}
}

434
src/views/AccountViewView.vue

@ -310,6 +310,21 @@ interface Notification {
text: string;
}
interface IAccount {
did: string;
publicKeyHex: string;
privateHex: string;
derivationPath: string;
}
interface SettingsType {
activeDid?: string;
apiServer?: string;
firstName?: string;
lastName?: string;
showContactGivesInline?: boolean;
}
@Component({ components: { QuickNav } })
export default class AccountViewView extends Vue {
$notify!: (notification: Notification, timeout?: number) => void;
@ -339,23 +354,56 @@ export default class AccountViewView extends Vue {
alertMessage = "";
alertTitle = "";
public async getIdentity(activeDid: string) {
await accountsDB.open();
const account = await accountsDB.accounts
.where("did")
.equals(activeDid)
.first();
const identity = JSON.parse(account?.identity || "null");
return identity;
public async getIdentity(activeDid: string): Promise<IIdentifier | null> {
try {
// Open the accounts database
await accountsDB.open();
} catch (error) {
console.error("Failed to open accounts database:", error);
return null;
}
let account: { identity?: string } | undefined;
try {
// Search for the account with the matching DID (decentralized identifier)
account = await accountsDB.accounts
.where("did")
.equals(activeDid)
.first();
} catch (error) {
console.error("Failed to find account:", error);
return null;
}
// Return parsed identity or null if not found
return JSON.parse(account?.identity || "null");
}
public async getHeaders(identity: IIdentifier) {
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
return headers;
/**
* Asynchronously retrieves headers for HTTP requests.
*
* @param {IIdentifier} identity - The identity object for which to generate the headers.
* @returns {Promise<Record<string,string>>} A Promise that resolves to an object containing the headers.
*
* @throws Will throw an error if unable to generate an access token.
*/
public async getHeaders(
identity: IIdentifier,
): Promise<Record<string, string>> {
try {
const token = await accessToken(identity);
const headers: Record<string, string> = {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
};
return headers;
} catch (error) {
console.error("Failed to get headers:", error);
return Promise.reject(error);
}
}
// call fn, copy text to the clipboard, then redo fn after 2 seconds
@ -380,60 +428,82 @@ export default class AccountViewView extends Vue {
this.numAccounts = await accountsDB.accounts.count();
}
/**
* Async function executed when the component is created.
* Initializes the component's state with values from the database,
* handles identity-related tasks, and checks limitations.
*
* @throws Will display specific messages to the user based on different errors.
*/
async created() {
// Uncomment this to register this user on the test server.
// To manage within the vue devtools browser extension https://devtools.vuejs.org/
// assign this to a class variable, eg. "registerThisUser = testServerRegisterUser",
// select a component in the extension, and enter in the console: $vm.ctx.registerThisUser()
//testServerRegisterUser();
try {
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
this.activeDid = settings?.activeDid || "";
this.apiServer = settings?.apiServer || "";
this.apiServerInput = settings?.apiServer || "";
this.firstName = settings?.firstName || "";
this.lastName = settings?.lastName || "";
this.showContactGives = !!settings?.showContactGivesInline;
const identity = await this.getIdentity(this.activeDid);
// Initialize component state with values from the database or defaults
this.initializeState(settings);
// Get and process the identity
const identity = await this.getIdentity(this.activeDid);
if (identity) {
this.publicHex = identity.keys[0].publicKeyHex;
this.publicBase64 = Buffer.from(this.publicHex, "hex").toString(
"base64",
);
this.derivationPath = identity.keys[0].meta.derivationPath;
db.settings.update(MASTER_SETTINGS_KEY, {
activeDid: identity.did,
});
this.checkLimitsFor(identity);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) {
if (
err.message ===
"Attempted to load account records with no identity available."
) {
this.limitsMessage = "No identity.";
this.loadingLimits = false;
} else {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Creating Account",
text: "Clear your cache and start over (after data backup).",
},
-1,
);
console.error(
"Telling user to clear cache at page create because:",
err,
);
this.processIdentity(identity);
}
} catch (err: unknown) {
this.handleError(err);
}
}
/**
* Initializes component state with values from the database or defaults.
* @param {SettingsType} settings - Object containing settings from the database.
*/
initializeState(settings: SettingsType | undefined) {
this.activeDid = settings?.activeDid || "";
this.apiServer = settings?.apiServer || "";
this.apiServerInput = settings?.apiServer || "";
this.firstName = settings?.firstName || "";
this.lastName = settings?.lastName || "";
this.showContactGives = !!settings?.showContactGivesInline;
}
/**
* Processes the identity and updates the component's state.
* @param {IdentityType} identity - Object containing identity information.
*/
processIdentity(identity: IdentityType) {
this.publicHex = identity.keys[0].publicKeyHex;
this.publicBase64 = Buffer.from(this.publicHex, "hex").toString("base64");
this.derivationPath = identity.keys[0].meta.derivationPath;
db.settings.update(MASTER_SETTINGS_KEY, {
activeDid: identity.did,
});
this.checkLimitsFor(identity);
}
/**
* Handles errors and updates the component's state accordingly.
* @param {Error} err - The error object.
*/
handleError(err: unknown) {
if (
err.message ===
"Attempted to load account records with no identity available."
) {
this.limitsMessage = "No identity.";
this.loadingLimits = false;
} else {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Creating Account",
text: "Clear your cache and start over (after data backup).",
},
-1,
);
console.error("Telling user to clear cache at page create because:", err);
}
}
@ -460,41 +530,96 @@ export default class AccountViewView extends Vue {
}
}
/**
* Asynchronously exports the database into a downloadable JSON file.
*
* @throws Will notify the user if there is an export error.
*/
public async exportDatabase() {
try {
const blob = await db.export({ prettyJson: true });
const url = URL.createObjectURL(blob);
// Generate the blob from the database
const blob = await this.generateDatabaseBlob();
// Create a temporary URL for the blob
const url = this.createBlobURL(blob);
const downloadAnchor = this.$refs.downloadLink as HTMLAnchorElement;
downloadAnchor.href = url;
downloadAnchor.download = db.name + "-backup.json";
downloadAnchor.click();
// Trigger the download
this.downloadDatabaseBackup(url);
// Revoke the temporary URL
URL.revokeObjectURL(url);
this.$notify(
{
group: "alert",
type: "toast",
title: "Download Started",
text: "See your downloads directory for the backup.",
},
5000,
);
// Notify the user that the download has started
this.notifyDownloadStarted();
} catch (error) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Export Error",
text: "See console logs for more info.",
},
-1,
);
console.error("Export Error:", error);
this.handleExportError(error);
}
}
/**
* Generates a blob object representing the database.
*
* @returns {Promise<Blob>} The generated blob object.
*/
private async generateDatabaseBlob(): Promise<Blob> {
return await db.export({ prettyJson: true });
}
/**
* Creates a temporary URL for a blob object.
*
* @param {Blob} blob - The blob object.
* @returns {string} The temporary URL for the blob.
*/
private createBlobURL(blob: Blob): string {
return URL.createObjectURL(blob);
}
/**
* Triggers the download of the database backup.
*
* @param {string} url - The temporary URL for the blob.
*/
private downloadDatabaseBackup(url: string) {
const downloadAnchor = this.$refs.downloadLink as HTMLAnchorElement;
downloadAnchor.href = url;
downloadAnchor.download = `${db.name}-backup.json`;
downloadAnchor.click();
}
/**
* Notifies the user that the download has started.
*/
private notifyDownloadStarted() {
this.$notify(
{
group: "alert",
type: "toast",
title: "Download Started",
text: "See your downloads directory for the backup.",
},
5000,
);
}
/**
* Handles errors during the database export process.
*
* @param {Error} error - The error object.
*/
private handleExportError(error: unknown) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Export Error",
text: "See console logs for more info.",
},
-1,
);
console.error("Export Error:", error);
}
async checkLimits() {
const identity = await this.getIdentity(this.activeDid);
if (identity) {
@ -502,66 +627,115 @@ export default class AccountViewView extends Vue {
}
}
async checkLimitsFor(identity: IIdentifier) {
/**
* Asynchronously checks rate limits for the given identity.
*
* Updates component state variables `limits`, `limitsMessage`, and `loadingLimits`.
*/
public async checkLimitsFor(identity: IIdentifier) {
this.loadingLimits = true;
this.limitsMessage = "";
try {
const url = this.apiServer + "/api/report/rateLimits";
const headers = await this.getHeaders(identity);
const resp = await this.axios.get(url, { headers });
// axios throws an exception on a 400
const resp = await this.fetchRateLimits(identity);
if (resp.status === 200) {
this.limits = resp.data;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
if (
error.message ===
"Attempted to load Give records with no identity available."
) {
this.limitsMessage = "No identity.";
this.loadingLimits = false;
} else {
const serverError = error as AxiosError;
console.error("Bad response retrieving limits: ", serverError);
const data = (serverError.response &&
serverError.response.data) as ErrorResponse;
this.limitsMessage = data?.error?.message || "Bad server response.";
}
} catch (error) {
this.handleRateLimitsError(error);
}
this.loadingLimits = false;
}
async switchAccount(accountNum: number) {
// 0 means none
/**
* Fetches rate limits from the server.
*
* @param {IIdentifier} identity - The identity object to check rate limits for.
* @returns {Promise<AxiosResponse>} The Axios response object.
*/
private async fetchRateLimits(identity: IIdentifier) {
const url = `${this.apiServer}/api/report/rateLimits`;
const headers = await this.getHeaders(identity);
return await this.axios.get(url, { headers });
}
/**
* Handles errors that occur while fetching rate limits.
*
* @param {AxiosError | Error} error - The error object.
*/
private handleRateLimitsError(error: AxiosError | Error) {
if (error instanceof AxiosError) {
const data = error.response?.data as ErrorResponse;
this.limitsMessage = data?.error?.message || "Bad server response.";
console.error("Bad response retrieving limits:", error);
} else if (
error.message ===
"Attempted to load Give records with no identity available."
) {
this.limitsMessage = "No identity.";
}
}
/**
* Asynchronously switches the active account based on the provided account number.
*
* @param {number} accountNum - The account number to switch to. 0 means none.
*/
public async switchAccount(accountNum: number) {
await db.open(); // Assumes db needs to be open for both cases
if (accountNum === 0) {
await db.open();
db.settings.update(MASTER_SETTINGS_KEY, {
activeDid: undefined,
});
this.activeDid = "";
this.derivationPath = "";
this.publicHex = "";
this.publicBase64 = "";
this.switchToNoAccount();
} else {
await accountsDB.open();
const accounts = await accountsDB.accounts.toArray();
const account = accounts[accountNum - 1];
await this.switchToAccountNumber(accountNum);
}
}
await db.open();
db.settings.update(MASTER_SETTINGS_KEY, {
activeDid: account.did,
});
/**
* Switches to no active account and clears relevant properties.
*/
private async switchToNoAccount() {
await db.settings.update(MASTER_SETTINGS_KEY, { activeDid: undefined });
this.clearActiveAccountProperties();
}
this.activeDid = account.did;
this.derivationPath = account.derivationPath;
this.publicHex = account.publicKeyHex;
this.publicBase64 = Buffer.from(this.publicHex, "hex").toString("base64");
}
/**
* Clears properties related to the active account.
*/
private clearActiveAccountProperties() {
this.activeDid = "";
this.derivationPath = "";
this.publicHex = "";
this.publicBase64 = "";
}
/**
* Switches to an account based on its number in the list.
*
* @param {number} accountNum - The account number to switch to.
*/
private async switchToAccountNumber(accountNum: number) {
await accountsDB.open();
const accounts = await accountsDB.accounts.toArray();
const account = accounts[accountNum - 1];
await db.settings.update(MASTER_SETTINGS_KEY, { activeDid: account.did });
this.updateActiveAccountProperties(account);
}
/**
* Updates properties related to the active account.
*
* @param {AccountType} account - The account object.
*/
private updateActiveAccountProperties(account: IAccount) {
this.activeDid = account.did;
this.derivationPath = account.derivationPath;
this.publicHex = account.publicKeyHex;
this.publicBase64 = Buffer.from(this.publicHex, "hex").toString("base64");
}
public showContactGivesClassNames() {

Loading…
Cancel
Save