@ -310,6 +310,21 @@ interface Notification {
text : string ;
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 } } )
@ Component ( { components : { QuickNav } } )
export default class AccountViewView extends Vue {
export default class AccountViewView extends Vue {
$notify ! : ( notification : Notification , timeout ? : number ) => void ;
$notify ! : ( notification : Notification , timeout ? : number ) => void ;
@ -339,23 +354,56 @@ export default class AccountViewView extends Vue {
alertMessage = "" ;
alertMessage = "" ;
alertTitle = "" ;
alertTitle = "" ;
public async getIdentity ( activeDid : string ) {
public async getIdentity ( activeDid : string ) : Promise < IIdentifier | null > {
await accountsDB . open ( ) ;
try {
const account = await accountsDB . accounts
/ / O p e n t h e a c c o u n t s d a t a b a s e
. where ( "did" )
await accountsDB . open ( ) ;
. equals ( activeDid )
} catch ( error ) {
. first ( ) ;
console . error ( "Failed to open accounts database:" , error ) ;
const identity = JSON . parse ( account ? . identity || "null" ) ;
return null ;
return identity ;
}
let account : { identity ? : string } | undefined ;
try {
/ / S e a r c h f o r t h e a c c o u n t w i t h t h e m a t c h i n g D I D ( d e c e n t r a l i z e d i d e n t i f i e r )
account = await accountsDB . accounts
. where ( "did" )
. equals ( activeDid )
. first ( ) ;
} catch ( error ) {
console . error ( "Failed to find account:" , error ) ;
return null ;
}
/ / R e t u r n p a r s e d i d e n t i t y o r n u l l i f n o t f o u n d
return JSON . parse ( account ? . identity || "null" ) ;
}
}
public async getHeaders ( identity : IIdentifier ) {
/ * *
const token = await accessToken ( identity ) ;
* Asynchronously retrieves headers for HTTP requests .
const headers = {
*
"Content-Type" : "application/json" ,
* @ param { IIdentifier } identity - The identity object for which to generate the headers .
Authorization : "Bearer " + token ,
* @ returns { Promise < Record < string , string > > } A Promise that resolves to an object containing the headers .
} ;
*
return 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 ) ;
}
}
}
/ / c a l l f n , c o p y t e x t t o t h e c l i p b o a r d , t h e n r e d o f n a f t e r 2 s e c o n d s
/ / c a l l f n , c o p y t e x t t o t h e c l i p b o a r d , t h e n r e d o f n a f t e r 2 s e c o n d s
@ -380,60 +428,82 @@ export default class AccountViewView extends Vue {
this . numAccounts = await accountsDB . accounts . count ( ) ;
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 ( ) {
async created ( ) {
/ / U n c o m m e n t t h i s t o r e g i s t e r t h i s u s e r o n t h e t e s t s e r v e r .
/ / T o m a n a g e w i t h i n t h e v u e d e v t o o l s b r o w s e r e x t e n s i o n h t t p s : / / d e v t o o l s . v u e j s . o r g /
/ / a s s i g n t h i s t o a c l a s s v a r i a b l e , e g . " r e g i s t e r T h i s U s e r = t e s t S e r v e r R e g i s t e r U s e r " ,
/ / s e l e c t a c o m p o n e n t i n t h e e x t e n s i o n , a n d e n t e r i n t h e c o n s o l e : $ v m . c t x . r e g i s t e r T h i s U s e r ( )
/ / t e s t S e r v e r R e g i s t e r U s e r ( ) ;
try {
try {
await db . open ( ) ;
await db . open ( ) ;
const settings = await db . settings . get ( MASTER_SETTINGS_KEY ) ;
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 ) ;
/ / I n i t i a l i z e c o m p o n e n t s t a t e w i t h v a l u e s f r o m t h e d a t a b a s e o r d e f a u l t s
this . initializeState ( settings ) ;
/ / G e t a n d p r o c e s s t h e i d e n t i t y
const identity = await this . getIdentity ( this . activeDid ) ;
if ( identity ) {
if ( identity ) {
this . publicHex = identity . keys [ 0 ] . publicKeyHex ;
this . processIdentity ( identity ) ;
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 ) ;
}
/ / e s l i n t - d i s a b l e - n e x t - l i n e @ t y p e s c r i p t - e s l i n t / n o - e x p l i c i t - a n y
} 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 ,
) ;
}
}
} 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 ( ) {
public async exportDatabase ( ) {
try {
try {
const blob = await db . export ( { prettyJson : true } ) ;
/ / G e n e r a t e t h e b l o b f r o m t h e d a t a b a s e
const url = URL . createObjectURL ( blob ) ;
const blob = await this . generateDatabaseBlob ( ) ;
/ / C r e a t e a t e m p o r a r y U R L f o r t h e b l o b
const url = this . createBlobURL ( blob ) ;
const downloadAnchor = this . $refs . downloadLink as HTMLAnchorElement ;
/ / T r i g g e r t h e d o w n l o a d
downloadAnchor . href = url ;
this . downloadDatabaseBackup ( url ) ;
downloadAnchor . download = db . name + "-backup.json" ;
downloadAnchor . click ( ) ;
/ / R e v o k e t h e t e m p o r a r y U R L
URL . revokeObjectURL ( url ) ;
URL . revokeObjectURL ( url ) ;
this . $notify (
/ / N o t i f y t h e u s e r t h a t t h e d o w n l o a d h a s s t a r t e d
{
this . notifyDownloadStarted ( ) ;
group : "alert" ,
type : "toast" ,
title : "Download Started" ,
text : "See your downloads directory for the backup." ,
} ,
5000 ,
) ;
} catch ( error ) {
} catch ( error ) {
this . $notify (
this . handleExportError ( error ) ;
{
group : "alert" ,
type : "danger" ,
title : "Export Error" ,
text : "See console logs for more info." ,
} ,
- 1 ,
) ;
console . error ( "Export Error:" , 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 ( ) {
async checkLimits ( ) {
const identity = await this . getIdentity ( this . activeDid ) ;
const identity = await this . getIdentity ( this . activeDid ) ;
if ( identity ) {
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 . loadingLimits = true ;
this . limitsMessage = "" ;
this . limitsMessage = "" ;
try {
try {
const url = this . apiServer + "/api/report/rateLimits" ;
const resp = await this . fetchRateLimits ( identity ) ;
const headers = await this . getHeaders ( identity ) ;
const resp = await this . axios . get ( url , { headers } ) ;
/ / a x i o s t h r o w s a n e x c e p t i o n o n a 4 0 0
if ( resp . status === 200 ) {
if ( resp . status === 200 ) {
this . limits = resp . data ;
this . limits = resp . data ;
}
}
/ / e s l i n t - d i s a b l e - n e x t - l i n e @ t y p e s c r i p t - e s l i n t / n o - e x p l i c i t - a n y
} catch ( error ) {
} catch ( error : any ) {
this . handleRateLimitsError ( error ) ;
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." ;
}
}
}
this . loadingLimits = false ;
this . loadingLimits = false ;
}
}
async switchAccount ( accountNum : number ) {
/ * *
/ / 0 m e a n s n o n e
* 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 ( ) ; / / A s s u m e s d b n e e d s t o b e o p e n f o r b o t h c a s e s
if ( accountNum === 0 ) {
if ( accountNum === 0 ) {
await db . open ( ) ;
this . switchToNoAccount ( ) ;
db . settings . update ( MASTER_SETTINGS_KEY , {
activeDid : undefined ,
} ) ;
this . activeDid = "" ;
this . derivationPath = "" ;
this . publicHex = "" ;
this . publicBase64 = "" ;
} else {
} else {
await accountsDB . open ( ) ;
await this . switchToAccountNumber ( accountNum ) ;
const accounts = await accountsDB . accounts . toArray ( ) ;
}
const account = accounts [ accountNum - 1 ] ;
}
await db . open ( ) ;
/ * *
db . settings . update ( MASTER_SETTINGS_KEY , {
* Switches to no active account and clears relevant properties .
activeDid : account . did ,
* /
} ) ;
private async switchToNoAccount ( ) {
await db . settings . update ( MASTER_SETTINGS_KEY , { activeDid : undefined } ) ;
this . clearActiveAccountProperties ( ) ;
}
this . activeDid = account . did ;
/ * *
this . derivationPath = account . derivationPath ;
* Clears properties related to the active account .
this . publicHex = account . publicKeyHex ;
* /
this . publicBase64 = Buffer . from ( this . publicHex , "hex" ) . toString ( "base64" ) ;
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 ( ) {
public showContactGivesClassNames ( ) {