diff --git a/sample.txt b/sample.txt new file mode 100644 index 000000000..903c3c3f2 --- /dev/null +++ b/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'. + 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 | /** + diff --git a/src/db/tables/accounts.ts b/src/db/tables/accounts.ts index d31f16084..63f4c6a53 100644 --- a/src/db/tables/accounts.ts +++ b/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", diff --git a/src/libs/endorserServer.ts b/src/libs/endorserServer.ts index 3b6704fa8..f651310db 100644 --- a/src/libs/endorserServer.ts +++ b/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.", + }, + }; } } diff --git a/src/views/AccountViewView.vue b/src/views/AccountViewView.vue index c17be54f3..4172eeacb 100644 --- a/src/views/AccountViewView.vue +++ b/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 { + 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>} 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> { + try { + const token = await accessToken(identity); + + const headers: Record = { + "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} The generated blob object. + */ + private async generateDatabaseBlob(): Promise { + 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} 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() {