Browse Source

Merge branch 'passkey-cache'

playwright-pwa-install-test
Trent Larson 2 months ago
parent
commit
bfa30d691b
  1. 5
      src/db/tables/settings.ts
  2. 2
      src/libs/crypto/index.ts
  3. 10
      src/libs/crypto/vc/passkeyDidPeer.ts
  4. 52
      src/libs/endorserServer.ts
  5. 16
      src/libs/util.ts
  6. 250
      src/views/AccountViewView.vue
  7. 15
      src/views/ContactAmountsView.vue
  8. 10
      src/views/GiftedDetails.vue
  9. 9
      src/views/HelpView.vue
  10. 77
      src/views/HomeView.vue
  11. 24
      src/views/NewEditProjectView.vue
  12. 6
      src/views/ProjectViewView.vue
  13. 28
      src/views/ProjectsView.vue
  14. 7
      src/views/SharedPhotoView.vue
  15. 7
      src/views/StartView.vue

5
src/db/tables/settings.ts

@ -26,6 +26,7 @@ export type Settings = {
lastName?: string; // deprecated - put all names in firstName lastName?: string; // deprecated - put all names in firstName
lastNotifiedClaimId?: string; lastNotifiedClaimId?: string;
lastViewedClaimId?: string; lastViewedClaimId?: string;
passkeyExpirationMinutes?: number; // passkey access token time-to-live in minutes
profileImageUrl?: string; profileImageUrl?: string;
reminderTime?: number; // Time in milliseconds since UNIX epoch for reminders reminderTime?: number; // Time in milliseconds since UNIX epoch for reminders
reminderOn?: boolean; // Toggle to enable or disable reminders reminderOn?: boolean; // Toggle to enable or disable reminders
@ -46,7 +47,7 @@ export type Settings = {
}; };
export function isAnyFeedFilterOn(settings: Settings): boolean { export function isAnyFeedFilterOn(settings: Settings): boolean {
return !!(settings.filterFeedByNearby || settings.filterFeedByVisible); return !!(settings?.filterFeedByNearby || settings?.filterFeedByVisible);
} }
/** /**
@ -60,3 +61,5 @@ export const SettingsSchema = {
* Constants. * Constants.
*/ */
export const MASTER_SETTINGS_KEY = 1; export const MASTER_SETTINGS_KEY = 1;
export const DEFAULT_PASSKEY_EXPIRATION_MINUTES = 15;

2
src/libs/crypto/index.ts

@ -85,7 +85,7 @@ export const generateSeed = (): string => {
}; };
/** /**
* Retreive an access token * Retrieve an access token, or "" if no DID is provided.
* *
* @return {*} * @return {*}
*/ */

10
src/libs/crypto/vc/passkeyDidPeer.ts

@ -411,13 +411,21 @@ async function peerDidToDidDocument(did: string): Promise<DIDResolutionResult> {
} }
// this is basically hard-coded from https://www.w3.org/TR/did-core/#example-various-verification-method-types // this is basically hard-coded from https://www.w3.org/TR/did-core/#example-various-verification-method-types
// (another reference is the @aviarytech/did-peer resolver) // (another reference is the @aviarytech/did-peer resolver)
/**
* Looks like JsonWebKey2020 isn't too difficult:
* - change context security/suites link to jws-2020/v1
* - change publicKeyMultibase to publicKeyJwk generated with cborToKeys
* - change type to JsonWebKey2020
*/
const id = did.split(":")[2]; const id = did.split(":")[2];
const multibase = id.slice(1); const multibase = id.slice(1);
const encnumbasis = multibase.slice(1); const encnumbasis = multibase.slice(1);
const didDocument = { const didDocument = {
"@context": [ "@context": [
"https://www.w3.org/ns/did/v1", "https://www.w3.org/ns/did/v1",
"https://w3id.org/security/suites/jws-2020/v1", "https://w3id.org/security/suites/secp256k1-2019/v1",
], ],
assertionMethod: [did + "#" + encnumbasis], assertionMethod: [did + "#" + encnumbasis],
authentication: [did + "#" + encnumbasis], authentication: [did + "#" + encnumbasis],

52
src/libs/endorserServer.ts

@ -5,9 +5,10 @@ import * as R from "ramda";
import { DEFAULT_IMAGE_API_SERVER } from "@/constants/app"; import { DEFAULT_IMAGE_API_SERVER } from "@/constants/app";
import { Contact } from "@/db/tables/contacts"; import { Contact } from "@/db/tables/contacts";
import { accessToken } from "@/libs/crypto"; import { accessToken } from "@/libs/crypto";
import { NonsensitiveDexie } from "@/db/index"; import { db, NonsensitiveDexie } from "@/db/index";
import { getAccount } from "@/libs/util"; import { getAccount, getPasskeyExpirationSeconds } from "@/libs/util";
import { createEndorserJwtForKey, KeyMeta } from "@/libs/crypto/vc"; import { createEndorserJwtForKey, KeyMeta } from "@/libs/crypto/vc";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
export const SCHEMA_ORG_CONTEXT = "https://schema.org"; export const SCHEMA_ORG_CONTEXT = "https://schema.org";
// the object in RegisterAction claims // the object in RegisterAction claims
@ -447,12 +448,57 @@ export function didInfo(
return didInfoForContact(did, activeDid, contact, allMyDids).displayName; return didInfoForContact(did, activeDid, contact, allMyDids).displayName;
} }
let passkeyAccessToken: string = "";
let passkeyTokenExpirationEpochSeconds: number = 0;
export function clearPasskeyToken() {
passkeyAccessToken = "";
passkeyTokenExpirationEpochSeconds = 0;
}
export function tokenExpiryTimeDescription() {
if (
!passkeyAccessToken ||
passkeyTokenExpirationEpochSeconds < new Date().getTime() / 1000
) {
return "Token has expired";
} else {
return (
"Token expires at " +
new Date(passkeyTokenExpirationEpochSeconds * 1000).toLocaleString()
);
}
}
/**
* Get the headers for a request, potentially including Authorization
*/
export async function getHeaders(did?: string) { export async function getHeaders(did?: string) {
const headers: { "Content-Type": string; Authorization?: string } = { const headers: { "Content-Type": string; Authorization?: string } = {
"Content-Type": "application/json", "Content-Type": "application/json",
}; };
if (did) { if (did) {
const token = await accessToken(did); let token;
const account = await getAccount(did);
if (account?.passkeyCredIdHex) {
if (
passkeyAccessToken &&
passkeyTokenExpirationEpochSeconds > Date.now() / 1000
) {
// there's an active current passkey token
token = passkeyAccessToken;
} else {
// there's no current passkey token or it's expired
token = await accessToken(did);
passkeyAccessToken = token;
const passkeyExpirationSeconds = await getPasskeyExpirationSeconds();
passkeyTokenExpirationEpochSeconds =
Date.now() / 1000 + passkeyExpirationSeconds;
}
} else {
token = await accessToken(did);
}
headers["Authorization"] = "Bearer " + token; headers["Authorization"] = "Bearer " + token;
} else { } else {
// it's often OK to request without auth; we assume necessary checks are done earlier // it's often OK to request without auth; we assume necessary checks are done earlier

16
src/libs/util.ts

@ -1,21 +1,20 @@
// many of these are also found in endorser-mobile utility.ts // many of these are also found in endorser-mobile utility.ts
import axios, { AxiosResponse } from "axios"; import axios, { AxiosResponse } from "axios";
import { IIdentifier } from "@veramo/core";
import { useClipboard } from "@vueuse/core"; import { useClipboard } from "@vueuse/core";
import { DEFAULT_PUSH_SERVER } from "@/constants/app"; import { DEFAULT_PUSH_SERVER } from "@/constants/app";
import { accountsDB, db } from "@/db/index"; import { accountsDB, db } from "@/db/index";
import { Account } from "@/db/tables/accounts"; import { Account } from "@/db/tables/accounts";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import {DEFAULT_PASSKEY_EXPIRATION_MINUTES, MASTER_SETTINGS_KEY} from "@/db/tables/settings";
import { deriveAddress, generateSeed, newIdentifier } from "@/libs/crypto"; import { deriveAddress, generateSeed, newIdentifier } from "@/libs/crypto";
import { GenericCredWrapper, containsHiddenDid } from "@/libs/endorserServer"; import { GenericCredWrapper, containsHiddenDid } from "@/libs/endorserServer";
import * as serverUtil from "@/libs/endorserServer"; import * as serverUtil from "@/libs/endorserServer";
import { registerCredential } from "@/libs/crypto/vc/passkeyDidPeer"; import { registerCredential } from "@/libs/crypto/vc/passkeyDidPeer";
import { Buffer } from "buffer"; import { Buffer } from "buffer";
import {KeyMeta} from "@/libs/crypto/vc"; import { KeyMeta } from "@/libs/crypto/vc";
import {createPeerDid} from "@/libs/crypto/vc/didPeer"; import { createPeerDid } from "@/libs/crypto/vc/didPeer";
export const PRIVACY_MESSAGE = export const PRIVACY_MESSAGE =
"The data you send will be visible to the world -- except: your IDs and the IDs of anyone you tag will stay private, only visible to them and others you explicitly allow."; "The data you send will be visible to the world -- except: your IDs and the IDs of anyone you tag will stay private, only visible to them and others you explicitly allow.";
@ -273,6 +272,15 @@ export const registerSaveAndActivatePasskey = async (
return account; return account;
}; };
export const getPasskeyExpirationSeconds = async (): Promise<number> => {
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
const passkeyExpirationSeconds =
(settings?.passkeyExpirationMinutes ?? DEFAULT_PASSKEY_EXPIRATION_MINUTES) *
60;
return passkeyExpirationSeconds;
};
export const sendTestThroughPushServer = async ( export const sendTestThroughPushServer = async (
subscriptionJSON: PushSubscriptionJSON, subscriptionJSON: PushSubscriptionJSON,
skipFilter: boolean, skipFilter: boolean,

250
src/views/AccountViewView.vue

@ -152,7 +152,7 @@
<div class="text-blue-500 text-sm font-bold"> <div class="text-blue-500 text-sm font-bold">
<router-link :to="{ path: '/did/' + encodeURIComponent(activeDid) }"> <router-link :to="{ path: '/did/' + encodeURIComponent(activeDid) }">
Activity Your Activity
</router-link> </router-link>
</div> </div>
</div> </div>
@ -216,7 +216,6 @@
<div class="mb-2 font-bold">Location</div> <div class="mb-2 font-bold">Location</div>
<router-link <router-link
:to="{ name: 'search-area' }" :to="{ name: 'search-area' }"
v-if="activeDid"
class="block w-full text-center text-m bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-2 mt-6" class="block w-full text-center text-m bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-2 mt-6"
> >
Set Search Area Set Search Area
@ -622,6 +621,26 @@
</button> </button>
</div> </div>
<div class="flex justify-between">
<span>
<span class="text-slate-500 text-sm font-bold mb-2">
Passkey Expiration Minutes
</span>
<br />
<span class="text-sm ml-2">
{{ passkeyExpirationDescription }}
</span>
</span>
<div class="relative ml-2">
<input
type="number"
class="border border-slate-400 rounded px-2 py-2 text-center w-20"
v-model="passkeyExpirationMinutes"
@change="updatePasskeyExpiration"
/>
</div>
</div>
<label <label
for="toggleShowGeneralAdvanced" for="toggleShowGeneralAdvanced"
class="flex items-center justify-between cursor-pointer mt-4" class="flex items-center justify-between cursor-pointer mt-4"
@ -667,7 +686,7 @@ import ImageMethodDialog from "@/components/ImageMethodDialog.vue";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
import TopMessage from "@/components/TopMessage.vue"; import TopMessage from "@/components/TopMessage.vue";
import { import {
AppString, AppString, DEFAULT_ENDORSER_API_SERVER,
DEFAULT_IMAGE_API_SERVER, DEFAULT_IMAGE_API_SERVER,
DEFAULT_PUSH_SERVER, DEFAULT_PUSH_SERVER,
IMAGE_TYPE_PROFILE, IMAGE_TYPE_PROFILE,
@ -675,14 +694,20 @@ import {
} from "@/constants/app"; } from "@/constants/app";
import { db, accountsDB } from "@/db/index"; import { db, accountsDB } from "@/db/index";
import { Account } from "@/db/tables/accounts"; import { Account } from "@/db/tables/accounts";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";
import { import {
DEFAULT_PASSKEY_EXPIRATION_MINUTES,
MASTER_SETTINGS_KEY,
Settings,
} from "@/db/tables/settings";
import {
clearPasskeyToken,
ErrorResponse, ErrorResponse,
EndorserRateLimits, EndorserRateLimits,
ImageRateLimits,
fetchEndorserRateLimits, fetchEndorserRateLimits,
fetchImageRateLimits, fetchImageRateLimits,
getHeaders,
ImageRateLimits,
tokenExpiryTimeDescription,
} from "@/libs/endorserServer"; } from "@/libs/endorserServer";
import { getAccount } from "@/libs/util"; import { getAccount } from "@/libs/util";
@ -713,6 +738,9 @@ export default class AccountViewView extends Vue {
limitsMessage = ""; limitsMessage = "";
loadingLimits = false; loadingLimits = false;
notificationMaybeChanged = false; notificationMaybeChanged = false;
passkeyExpirationDescription = "";
passkeyExpirationMinutes = DEFAULT_PASSKEY_EXPIRATION_MINUTES;
previousPasskeyExpirationMinutes = DEFAULT_PASSKEY_EXPIRATION_MINUTES;
profileImageUrl?: string; profileImageUrl?: string;
publicHex = ""; publicHex = "";
publicBase64 = ""; publicBase64 = "";
@ -745,12 +773,32 @@ export default class AccountViewView extends Vue {
await this.initializeState(); await this.initializeState();
await this.processIdentity(); await this.processIdentity();
this.passkeyExpirationDescription = tokenExpiryTimeDescription();
/**
* Beware! I've seen where this "ready" never resolves.
*/
const registration = await navigator.serviceWorker.ready; const registration = await navigator.serviceWorker.ready;
this.subscription = await registration.pushManager.getSubscription(); this.subscription = await registration.pushManager.getSubscription();
this.isSubscribed = !!this.subscription; this.isSubscribed = !!this.subscription;
console.log("Got to the end of 'mounted' call.");
/**
* Beware! I've seen where we never get to this point because "ready" never resolves.
*/
} catch (error) { } catch (error) {
console.error("Mount error:", error); console.error(
this.handleError(error); "Telling user to clear cache at page create because:",
error,
);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Loading Account",
text: "Clear your cache and start over (after data backup).",
},
-1,
);
} }
} }
@ -780,6 +828,10 @@ export default class AccountViewView extends Vue {
this.showContactGives = !!settings?.showContactGivesInline; this.showContactGives = !!settings?.showContactGivesInline;
this.hideRegisterPromptOnNewContact = this.hideRegisterPromptOnNewContact =
!!settings?.hideRegisterPromptOnNewContact; !!settings?.hideRegisterPromptOnNewContact;
this.passkeyExpirationMinutes =
(settings?.passkeyExpirationMinutes as number) ??
DEFAULT_PASSKEY_EXPIRATION_MINUTES;
this.previousPasskeyExpirationMinutes = this.passkeyExpirationMinutes;
this.showGeneralAdvanced = !!settings?.showGeneralAdvanced; this.showGeneralAdvanced = !!settings?.showGeneralAdvanced;
this.showShortcutBvc = !!settings?.showShortcutBvc; this.showShortcutBvc = !!settings?.showShortcutBvc;
this.warnIfProdServer = !!settings?.warnIfProdServer; this.warnIfProdServer = !!settings?.warnIfProdServer;
@ -835,11 +887,11 @@ export default class AccountViewView extends Vue {
this.publicHex = identity.keys[0].publicKeyHex; this.publicHex = identity.keys[0].publicKeyHex;
this.publicBase64 = Buffer.from(this.publicHex, "hex").toString("base64"); this.publicBase64 = Buffer.from(this.publicHex, "hex").toString("base64");
this.derivationPath = identity.keys[0].meta?.derivationPath as string; this.derivationPath = identity.keys[0].meta?.derivationPath as string;
this.checkLimitsFor(this.activeDid); await this.checkLimitsFor(this.activeDid);
} else if (account?.publicKeyHex) { } else if (account?.publicKeyHex) {
this.publicHex = account.publicKeyHex as string; this.publicHex = account.publicKeyHex as string;
this.publicBase64 = Buffer.from(this.publicHex, "hex").toString("base64"); this.publicBase64 = Buffer.from(this.publicHex, "hex").toString("base64");
this.checkLimitsFor(this.activeDid); await this.checkLimitsFor(this.activeDid);
} }
} }
@ -868,75 +920,18 @@ export default class AccountViewView extends Vue {
this.notificationMaybeChanged = true; this.notificationMaybeChanged = true;
} }
/**
* Handles errors and updates the component's state accordingly.
* @param {Error} err - The error object.
*/
handleError(err: unknown) {
if (
err instanceof Error &&
err.message ===
"Attempted to load account records with no identifier available."
) {
this.limitsMessage = "No identifier.";
} else {
console.error("Telling user to clear cache at page create because:", err);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Loading Account",
text: "Clear your cache and start over (after data backup).",
},
-1,
);
}
}
public async updateShowContactAmounts() { public async updateShowContactAmounts() {
try { await db.open();
await db.open(); await db.settings.update(MASTER_SETTINGS_KEY, {
await db.settings.update(MASTER_SETTINGS_KEY, { showContactGivesInline: this.showContactGives,
showContactGivesInline: this.showContactGives, });
});
} catch (err) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Updating Contact Setting",
text: "The setting may not have saved. Try again, maybe after restarting the app.",
},
-1,
);
console.error(
"Telling user to try again after contact-amounts setting update because:",
err,
);
}
} }
public async updateShowGeneralAdvanced() { public async updateShowGeneralAdvanced() {
try { await db.open();
await db.open(); await db.settings.update(MASTER_SETTINGS_KEY, {
await db.settings.update(MASTER_SETTINGS_KEY, { showGeneralAdvanced: this.showGeneralAdvanced,
showGeneralAdvanced: this.showGeneralAdvanced, });
});
} catch (err) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Updating Advanced Setting",
text: "The setting may not have saved. Try again, maybe after restarting the app.",
},
-1,
);
console.error(
"Telling user to try again after general-advanced setting update because:",
err,
);
}
} }
public async updateWarnIfProdServer(newSetting: boolean) { public async updateWarnIfProdServer(newSetting: boolean) {
@ -963,71 +958,35 @@ export default class AccountViewView extends Vue {
} }
public async updateWarnIfTestServer(newSetting: boolean) { public async updateWarnIfTestServer(newSetting: boolean) {
try { await db.open();
await db.open(); await db.settings.update(MASTER_SETTINGS_KEY, {
await db.settings.update(MASTER_SETTINGS_KEY, { warnIfTestServer: newSetting,
warnIfTestServer: newSetting, });
});
} catch (err) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Updating Test Warning",
text: "The setting may not have saved. Try again, maybe after restarting the app.",
},
-1,
);
console.error(
"Telling user to try again after test-server-warning setting update because:",
err,
);
}
} }
public async toggleHideRegisterPromptOnNewContact() { public async toggleHideRegisterPromptOnNewContact() {
const newSetting = !this.hideRegisterPromptOnNewContact; const newSetting = !this.hideRegisterPromptOnNewContact;
try { await db.open();
await db.open(); await db.settings.update(MASTER_SETTINGS_KEY, {
await db.settings.update(MASTER_SETTINGS_KEY, { hideRegisterPromptOnNewContact: newSetting,
hideRegisterPromptOnNewContact: newSetting, });
}); this.hideRegisterPromptOnNewContact = newSetting;
this.hideRegisterPromptOnNewContact = newSetting; }
} catch (err) {
this.$notify( public async updatePasskeyExpiration() {
{ await db.open();
group: "alert", await db.settings.update(MASTER_SETTINGS_KEY, {
type: "danger", passkeyExpirationMinutes: this.passkeyExpirationMinutes,
title: "Error Updating Setting", });
text: "The setting may not have saved. Try again, maybe after restarting the app.", clearPasskeyToken();
}, this.passkeyExpirationDescription = tokenExpiryTimeDescription();
-1,
);
console.error("Telling user to try again because:", err);
}
} }
public async updateShowShortcutBvc(newSetting: boolean) { public async updateShowShortcutBvc(newSetting: boolean) {
try { await db.open();
await db.open(); await db.settings.update(MASTER_SETTINGS_KEY, {
await db.settings.update(MASTER_SETTINGS_KEY, { showShortcutBvc: newSetting,
showShortcutBvc: newSetting, });
});
} catch (err) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Updating BVC Shortcut Setting",
text: "The setting may not have saved. Try again, maybe after restarting the app.",
},
-1,
);
console.error(
"Telling user to try again after BVC-shortcut setting update because:",
err,
);
}
} }
/** /**
@ -1220,7 +1179,7 @@ export default class AccountViewView extends Vue {
// the user was not known to be registered, but now they are (because we got no error) so let's record it // the user was not known to be registered, but now they are (because we got no error) so let's record it
try { try {
await db.open(); await db.open();
db.settings.update(MASTER_SETTINGS_KEY, { await db.settings.update(MASTER_SETTINGS_KEY, {
isRegistered: true, isRegistered: true,
}); });
this.isRegistered = true; this.isRegistered = true;
@ -1247,7 +1206,7 @@ export default class AccountViewView extends Vue {
try { try {
await db.open(); await db.open();
db.settings.update(MASTER_SETTINGS_KEY, { await db.settings.update(MASTER_SETTINGS_KEY, {
isRegistered: false, isRegistered: false,
}); });
this.isRegistered = false; this.isRegistered = false;
@ -1272,8 +1231,8 @@ export default class AccountViewView extends Vue {
(data?.error?.message as string) || "Bad server response."; (data?.error?.message as string) || "Bad server response.";
console.error( console.error(
"Got bad response retrieving limits, which usually means user isn't registered.", "Got bad response retrieving limits, which usually means user isn't registered.",
error,
); );
//console.error(error);
} else { } else {
this.limitsMessage = "Got an error retrieving limits."; this.limitsMessage = "Got an error retrieving limits.";
console.error("Got some error retrieving limits:", error); console.error("Got some error retrieving limits:", error);
@ -1350,7 +1309,7 @@ export default class AccountViewView extends Vue {
async onClickSaveApiServer() { async onClickSaveApiServer() {
await db.open(); await db.open();
db.settings.update(MASTER_SETTINGS_KEY, { await db.settings.update(MASTER_SETTINGS_KEY, {
apiServer: this.apiServerInput, apiServer: this.apiServerInput,
}); });
this.apiServer = this.apiServerInput; this.apiServer = this.apiServerInput;
@ -1358,7 +1317,7 @@ export default class AccountViewView extends Vue {
async onClickSavePushServer() { async onClickSavePushServer() {
await db.open(); await db.open();
db.settings.update(MASTER_SETTINGS_KEY, { await db.settings.update(MASTER_SETTINGS_KEY, {
webPushServer: this.webPushServerInput, webPushServer: this.webPushServerInput,
}); });
this.webPushServer = this.webPushServerInput; this.webPushServer = this.webPushServerInput;
@ -1377,7 +1336,7 @@ export default class AccountViewView extends Vue {
(this.$refs.imageMethodDialog as ImageMethodDialog).open( (this.$refs.imageMethodDialog as ImageMethodDialog).open(
async (imgUrl) => { async (imgUrl) => {
await db.open(); await db.open();
db.settings.update(MASTER_SETTINGS_KEY, { await db.settings.update(MASTER_SETTINGS_KEY, {
profileImageUrl: imgUrl, profileImageUrl: imgUrl,
}); });
this.profileImageUrl = imgUrl; this.profileImageUrl = imgUrl;
@ -1407,16 +1366,13 @@ export default class AccountViewView extends Vue {
return; return;
} }
try { try {
const token = await accessToken(this.activeDid); const headers = await getHeaders(this.activeDid);
this.passkeyExpirationDescription = tokenExpiryTimeDescription();
const response = await this.axios.delete( const response = await this.axios.delete(
DEFAULT_IMAGE_API_SERVER + DEFAULT_IMAGE_API_SERVER +
"/image/" + "/image/" +
encodeURIComponent(this.profileImageUrl), encodeURIComponent(this.profileImageUrl),
{ { headers },
headers: {
Authorization: `Bearer ${token}`,
},
},
); );
if (response.status === 204) { if (response.status === 204) {
// don't bother with a notification // don't bother with a notification
@ -1436,7 +1392,7 @@ export default class AccountViewView extends Vue {
} }
await db.open(); await db.open();
db.settings.update(MASTER_SETTINGS_KEY, { await db.settings.update(MASTER_SETTINGS_KEY, {
profileImageUrl: undefined, profileImageUrl: undefined,
}); });
@ -1448,7 +1404,7 @@ export default class AccountViewView extends Vue {
console.error("The image was already deleted:", error); console.error("The image was already deleted:", error);
await db.open(); await db.open();
db.settings.update(MASTER_SETTINGS_KEY, { await db.settings.update(MASTER_SETTINGS_KEY, {
profileImageUrl: undefined, profileImageUrl: undefined,
}); });

15
src/views/ContactAmountsView.vue

@ -55,7 +55,7 @@
{{ new Date(record.issuedAt).toLocaleString() }} {{ new Date(record.issuedAt).toLocaleString() }}
</td> </td>
<td class="p-1"> <td class="p-1">
<span v-if="record.agentDid == contact.did"> <span v-if="record.agentDid == contact?.did">
<div class="font-bold"> <div class="font-bold">
{{ displayAmount(record.unit, record.amount) }} {{ displayAmount(record.unit, record.amount) }}
<span v-if="record.amountConfirmed" title="Confirmed"> <span v-if="record.amountConfirmed" title="Confirmed">
@ -71,7 +71,7 @@
</span> </span>
</td> </td>
<td class="p-1"> <td class="p-1">
<span v-if="record.agentDid == contact.did"> <span v-if="record.agentDid == contact?.did">
<fa icon="arrow-left" class="text-slate-400 fa-fw" /> <fa icon="arrow-left" class="text-slate-400 fa-fw" />
</span> </span>
<span v-else> <span v-else>
@ -79,7 +79,7 @@
</span> </span>
</td> </td>
<td class="p-1"> <td class="p-1">
<span v-if="record.agentDid != contact.did"> <span v-if="record.agentDid != contact?.did">
<div class="font-bold"> <div class="font-bold">
{{ displayAmount(record.unit, record.amount) }} {{ displayAmount(record.unit, record.amount) }}
<span v-if="record.amountConfirmed" title="Confirmed"> <span v-if="record.amountConfirmed" title="Confirmed">
@ -105,7 +105,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { AxiosError } from "axios"; import { AxiosError, AxiosRequestHeaders } from "axios";
import * as R from "ramda"; import * as R from "ramda";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
@ -114,7 +114,6 @@ import { NotificationIface } from "@/constants/app";
import { accountsDB, db } from "@/db/index"; import { accountsDB, db } from "@/db/index";
import { Contact } from "@/db/tables/contacts"; import { Contact } from "@/db/tables/contacts";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";
import { import {
AgreeVerifiableCredential, AgreeVerifiableCredential,
createEndorserJwtVcFromClaim, createEndorserJwtVcFromClaim,
@ -271,11 +270,7 @@ export default class ContactAmountssView extends Vue {
// Make the xhr request payload // Make the xhr request payload
const payload = JSON.stringify({ jwtEncoded: vcJwt }); const payload = JSON.stringify({ jwtEncoded: vcJwt });
const url = this.apiServer + "/api/v2/claim"; const url = this.apiServer + "/api/v2/claim";
const token = await accessToken(this.activeDid); const headers = getHeaders(this.activeDid) as AxiosRequestHeaders;
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
try { try {
const resp = await this.axios.post(url, payload, { headers }); const resp = await this.axios.post(url, payload, { headers });

10
src/views/GiftedDetails.vue

@ -186,10 +186,10 @@ import {
constructGive, constructGive,
createAndSubmitGive, createAndSubmitGive,
didInfo, didInfo,
getHeaders,
getPlanFromCache, getPlanFromCache,
} from "@/libs/endorserServer"; } from "@/libs/endorserServer";
import * as libsUtil from "@/libs/util"; import * as libsUtil from "@/libs/util";
import { accessToken } from "@/libs/crypto";
import { Contact } from "@/db/tables/contacts"; import { Contact } from "@/db/tables/contacts";
@Component({ @Component({
@ -380,16 +380,12 @@ export default class GiftedDetails extends Vue {
return; return;
} }
try { try {
const token = await accessToken(this.activeDid); const headers = await getHeaders(this.activeDid);
const response = await this.axios.delete( const response = await this.axios.delete(
DEFAULT_IMAGE_API_SERVER + DEFAULT_IMAGE_API_SERVER +
"/image/" + "/image/" +
encodeURIComponent(this.imageUrl), encodeURIComponent(this.imageUrl),
{ { headers },
headers: {
Authorization: `Bearer ${token}`,
},
},
); );
if (response.status === 204) { if (response.status === 204) {
// don't bother with a notification // don't bother with a notification

9
src/views/HelpView.vue

@ -24,16 +24,15 @@
<!-- eslint-disable prettier/prettier --> <!-- eslint-disable prettier/prettier -->
<div> <div>
<p> <p>
This app is a window into data that you and your friends own, focused on This app focuses on gifts & gratitude, using them to build cool things with your network.
gifts and collaboration.
</p> </p>
<h2 class="text-xl font-semibold">What is the idea here?</h2> <h2 class="text-xl font-semibold">What is the idea here?</h2>
<p> <p>
We are building networks of people who want to grow a giving society. We are building networks of people who want to grow a giving society.
First of all, you can see what people have given, and also recognize First of all, let's build gratitude: see what people have given, and recognize
gifts you've seen, in a way that leaves a permanent record -- one that gifts you've seen. This is done in a way that leaves a permanent record -- one that
came from you, and the recipient can prove it was for them. This is came from you, and that the recipient can prove it was for them. This is
personally gratifying, but it extends to broader work: volunteers get personally gratifying, but it extends to broader work: volunteers get
confirmation of activity, and selectively show off their contributions confirmation of activity, and selectively show off their contributions
and network. and network.

77
src/views/HomeView.vue

@ -77,58 +77,28 @@
<div v-else> <div v-else>
<!-- !isCreatingIdentifier --> <!-- !isCreatingIdentifier -->
<div <!-- They should have an identifier, even if it's an auto-generated one that they'll never use. -->
v-if="!activeDid" <div class="mb-4">
class="bg-amber-200 rounded-md text-center px-4 py-3 mb-4"
>
<div v-if="PASSKEYS_ENABLED">
<p class="text-lg mb-3">
Choose how to see info from your contacts or share contributions:
</p>
<div class="flex justify-between">
<button
class="block text-center text-md font-bold bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mt-2 px-2 py-3 rounded-md"
@click="generateIdentifier()"
>
Let me start the easiest (with a passkey).
</button>
<router-link
:to="{ name: 'start' }"
class="block text-center text-md font-bold bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mt-2 px-2 py-3 rounded-md"
>
Give me all the options.
</router-link>
</div>
</div>
<div v-else>
<p class="text-lg mb-3">
To recognize giving or collaborate, have someone register you:
</p>
<router-link
:to="{ name: 'contact-qr' }"
class="block text-center text-md font-bold bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mt-2 px-2 py-3 rounded-md"
>
Share your contact info.
</router-link>
</div>
</div>
<div v-else class="mb-4">
<!-- activeDid -->
<div <div
v-if="!isRegistered" v-if="!isRegistered"
class="bg-amber-200 rounded-md overflow-hidden text-center px-4 py-3 mb-4" class="bg-amber-200 rounded-md overflow-hidden text-center px-4 py-3 mb-4"
> >
<!-- activeDid && !isRegistered --> <!-- activeDid && !isRegistered -->
Someone must register you before you can give kudos or make offers To share, someone must register you.
or create projects... basically before doing anything.
<router-link <router-link
:to="{ name: 'contact-qr' }" :to="{ name: 'contact-qr' }"
class="block text-center text-md font-bold bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mt-2 px-2 py-3 rounded-md" class="block text-center text-md font-bold bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mt-2 px-2 py-3 rounded-md"
> >
Show Them Your Identifier Info Show Them Default Identifier Info
</router-link> </router-link>
<div v-if="PASSKEYS_ENABLED" class="flex justify-end w-full">
<router-link
:to="{ name: 'start' }"
class="block text-right text-md font-bold bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mt-2 px-2 py-3 rounded-md"
>
See all your options first
</router-link>
</div>
</div> </div>
<div v-else> <div v-else>
@ -340,7 +310,11 @@ import FeedFilters from "@/components/FeedFilters.vue";
import InfiniteScroll from "@/components/InfiniteScroll.vue"; import InfiniteScroll from "@/components/InfiniteScroll.vue";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
import TopMessage from "@/components/TopMessage.vue"; import TopMessage from "@/components/TopMessage.vue";
import { AppString, NotificationIface, PASSKEYS_ENABLED } from "@/constants/app"; import {
AppString,
NotificationIface,
PASSKEYS_ENABLED,
} from "@/constants/app";
import { db, accountsDB } from "@/db/index"; import { db, accountsDB } from "@/db/index";
import { Contact } from "@/db/tables/contacts"; import { Contact } from "@/db/tables/contacts";
import { import {
@ -359,7 +333,10 @@ import {
GiverReceiverInputInfo, GiverReceiverInputInfo,
GiveSummaryRecord, GiveSummaryRecord,
} from "@/libs/endorserServer"; } from "@/libs/endorserServer";
import { registerSaveAndActivatePasskey } from "@/libs/util"; import {
generateSaveAndActivateIdentity,
registerSaveAndActivatePasskey,
} from "@/libs/util";
interface GiveRecordWithContactInfo extends GiveSummaryRecord { interface GiveRecordWithContactInfo extends GiveSummaryRecord {
giver: { giver: {
@ -423,7 +400,14 @@ export default class HomeView extends Vue {
try { try {
await accountsDB.open(); await accountsDB.open();
const allAccounts = await accountsDB.accounts.toArray(); const allAccounts = await accountsDB.accounts.toArray();
this.allMyDids = allAccounts.map((acc) => acc.did); if (allAccounts.length > 0) {
this.allMyDids = allAccounts.map((acc) => acc.did);
} else {
this.isCreatingIdentifier = true;
const newDid = await generateSaveAndActivateIdentity();
this.isCreatingIdentifier = false;
this.allMyDids = [newDid];
}
await db.open(); await db.open();
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings; const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
@ -440,6 +424,7 @@ export default class HomeView extends Vue {
this.isAnyFeedFilterOn = isAnyFeedFilterOn(settings); this.isAnyFeedFilterOn = isAnyFeedFilterOn(settings);
// someone may have have registered after sharing contact info, so recheck // someone may have have registered after sharing contact info, so recheck
if (!this.isRegistered && this.activeDid) { if (!this.isRegistered && this.activeDid) {
try { try {
@ -481,7 +466,7 @@ export default class HomeView extends Vue {
} }
} }
async generateIdentifier() { async generatePasskeyIdentifier() {
this.isCreatingIdentifier = true; this.isCreatingIdentifier = true;
const account = await registerSaveAndActivatePasskey( const account = await registerSaveAndActivatePasskey(
AppString.APP_NAME + (this.givenName ? " - " + this.givenName : ""), AppString.APP_NAME + (this.givenName ? " - " + this.givenName : ""),

24
src/views/NewEditProjectView.vue

@ -173,7 +173,7 @@
<script lang="ts"> <script lang="ts">
import "leaflet/dist/leaflet.css"; import "leaflet/dist/leaflet.css";
import { AxiosError } from "axios"; import { AxiosError, AxiosRequestHeaders } from "axios";
import { DateTime } from "luxon"; import { DateTime } from "luxon";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { LMap, LMarker, LTileLayer } from "@vue-leaflet/vue-leaflet"; import { LMap, LMarker, LTileLayer } from "@vue-leaflet/vue-leaflet";
@ -183,9 +183,9 @@ import QuickNav from "@/components/QuickNav.vue";
import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "@/constants/app"; import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "@/constants/app";
import { accountsDB, db } from "@/db/index"; import { accountsDB, db } from "@/db/index";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";
import { import {
createEndorserJwtVcFromClaim, createEndorserJwtVcFromClaim,
getHeaders,
PlanVerifiableCredential, PlanVerifiableCredential,
} from "@/libs/endorserServer"; } from "@/libs/endorserServer";
import { useAppStore } from "@/store/app"; import { useAppStore } from "@/store/app";
@ -250,11 +250,7 @@ export default class NewEditProjectView extends Vue {
this.apiServer + this.apiServer +
"/api/claim/byHandle/" + "/api/claim/byHandle/" +
encodeURIComponent(this.projectId); encodeURIComponent(this.projectId);
const token = await accessToken(userDid); const headers = await getHeaders(userDid);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
try { try {
const resp = await this.axios.get(url, { headers }); const resp = await this.axios.get(url, { headers });
@ -309,16 +305,12 @@ export default class NewEditProjectView extends Vue {
return; return;
} }
try { try {
const token = await accessToken(this.activeDid); const headers = getHeaders(this.activeDid) as AxiosRequestHeaders;
const response = await this.axios.delete( const response = await this.axios.delete(
DEFAULT_IMAGE_API_SERVER + DEFAULT_IMAGE_API_SERVER +
"/image/" + "/image/" +
encodeURIComponent(this.imageUrl), encodeURIComponent(this.imageUrl),
{ { headers },
headers: {
Authorization: `Bearer ${token}`,
},
},
); );
if (response.status === 204) { if (response.status === 204) {
// don't bother with a notification // don't bother with a notification
@ -418,11 +410,7 @@ export default class NewEditProjectView extends Vue {
const payload = JSON.stringify({ jwtEncoded: vcJwt }); const payload = JSON.stringify({ jwtEncoded: vcJwt });
const url = this.apiServer + "/api/v2/claim"; const url = this.apiServer + "/api/v2/claim";
const token = await accessToken(issuerDid); const headers = await getHeaders(issuerDid);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
try { try {
const resp = await this.axios.post(url, payload, { headers }); const resp = await this.axios.post(url, payload, { headers });

6
src/views/ProjectViewView.vue

@ -416,7 +416,6 @@ import { accountsDB, db } from "@/db/index";
import { Account } from "@/db/tables/accounts"; import { Account } from "@/db/tables/accounts";
import { Contact } from "@/db/tables/contacts"; import { Contact } from "@/db/tables/contacts";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";
import * as libsUtil from "@/libs/util"; import * as libsUtil from "@/libs/util";
import { import {
BLANK_GENERIC_SERVER_RECORD, BLANK_GENERIC_SERVER_RECORD,
@ -583,11 +582,6 @@ export default class ProjectViewView extends Vue {
this.loadPlanFulfillersTo(); this.loadPlanFulfillersTo();
// now load fulfilled-by, a single project
if (this.activeDid) {
const token = await accessToken(this.activeDid);
headers["Authorization"] = "Bearer " + token;
}
const fulfilledByUrl = const fulfilledByUrl =
this.apiServer + this.apiServer +
"/api/v2/report/planFulfilledByPlan?planHandleId=" + "/api/v2/report/planFulfilledByPlan?planHandleId=" +

28
src/views/ProjectsView.vue

@ -233,13 +233,16 @@ import { Component, Vue } from "vue-facing-decorator";
import { NotificationIface } from "@/constants/app"; import { NotificationIface } from "@/constants/app";
import { accountsDB, db } from "@/db/index"; import { accountsDB, db } from "@/db/index";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";
import * as libsUtil from "@/libs/util"; import * as libsUtil from "@/libs/util";
import InfiniteScroll from "@/components/InfiniteScroll.vue"; import InfiniteScroll from "@/components/InfiniteScroll.vue";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
import ProjectIcon from "@/components/ProjectIcon.vue"; import ProjectIcon from "@/components/ProjectIcon.vue";
import TopMessage from "@/components/TopMessage.vue"; import TopMessage from "@/components/TopMessage.vue";
import { OfferSummaryRecord, PlanData } from "@/libs/endorserServer"; import {
getHeaders,
OfferSummaryRecord,
PlanData,
} from "@/libs/endorserServer";
import EntityIcon from "@/components/EntityIcon.vue"; import EntityIcon from "@/components/EntityIcon.vue";
@Component({ @Component({
@ -293,13 +296,9 @@ export default class ProjectsView extends Vue {
* @param url the url used to fetch the data * @param url the url used to fetch the data
* @param token Authorization token * @param token Authorization token
**/ **/
async projectDataLoader(url: string, token: string) { async projectDataLoader(url: string) {
const headers: { [key: string]: string } = {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
};
try { try {
const headers = await getHeaders(this.activeDid);
this.isLoading = true; this.isLoading = true;
const resp = await this.axios.get(url, { headers } as AxiosRequestConfig); const resp = await this.axios.get(url, { headers } as AxiosRequestConfig);
if (resp.status === 200 && resp.data.data) { if (resp.status === 200 && resp.data.data) {
@ -353,8 +352,7 @@ export default class ProjectsView extends Vue {
**/ **/
async loadProjects(activeDid?: string, urlExtra: string = "") { async loadProjects(activeDid?: string, urlExtra: string = "") {
const url = `${this.apiServer}/api/v2/report/plansByIssuer?${urlExtra}`; const url = `${this.apiServer}/api/v2/report/plansByIssuer?${urlExtra}`;
const token: string = await accessToken(activeDid); await this.projectDataLoader(url);
await this.projectDataLoader(url, token);
} }
/** /**
@ -392,11 +390,8 @@ export default class ProjectsView extends Vue {
* @param url the url used to fetch the data * @param url the url used to fetch the data
* @param token Authorization token * @param token Authorization token
**/ **/
async offerDataLoader(url: string, token: string) { async offerDataLoader(url: string) {
const headers: { [key: string]: string } = { const headers = getHeaders(this.activeDid);
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
};
try { try {
this.isLoading = true; this.isLoading = true;
@ -454,8 +449,7 @@ export default class ProjectsView extends Vue {
**/ **/
async loadOffers(issuerDid?: string, urlExtra: string = "") { async loadOffers(issuerDid?: string, urlExtra: string = "") {
const url = `${this.apiServer}/api/v2/report/offers?offeredByDid=${issuerDid}${urlExtra}`; const url = `${this.apiServer}/api/v2/report/offers?offeredByDid=${issuerDid}${urlExtra}`;
const token: string = await accessToken(issuerDid); await this.offerDataLoader(url);
await this.offerDataLoader(url, token);
} }
public computedOfferTabClassNames() { public computedOfferTabClassNames() {

7
src/views/SharedPhotoView.vue

@ -65,7 +65,7 @@ import {
} from "@/constants/app"; } from "@/constants/app";
import { db } from "@/db/index"; import { db } from "@/db/index";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto"; import { getHeaders } from "@/libs/endorserServer";
@Component({ components: { PhotoDialog, QuickNav } }) @Component({ components: { PhotoDialog, QuickNav } })
export default class SharedPhotoView extends Vue { export default class SharedPhotoView extends Vue {
@ -151,10 +151,7 @@ export default class SharedPhotoView extends Vue {
let result; let result;
try { try {
// send the image to the server // send the image to the server
const token = await accessToken(this.activeDid); const headers = await getHeaders(this.activeDid);
const headers = {
Authorization: "Bearer " + token,
};
const formData = new FormData(); const formData = new FormData();
formData.append( formData.append(
"image", "image",

7
src/views/StartView.vue

@ -27,7 +27,7 @@
<p class="text-center text-xl font-light"> <p class="text-center text-xl font-light">
How do you want to create this identifier? How do you want to create this identifier?
</p> </p>
<p class="text-center font-light mt-6"> <p v-if="PASSKEYS_ENABLED" class="text-center font-light mt-6">
A <strong>passkey</strong> is easy to manage, though it is less A <strong>passkey</strong> is easy to manage, though it is less
interoperable with other systems for advanced uses. interoperable with other systems for advanced uses.
<a <a
@ -49,6 +49,7 @@
</p> </p>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2 mt-4"> <div class="grid grid-cols-1 sm:grid-cols-2 gap-2 mt-4">
<a <a
v-if="PASSKEYS_ENABLED"
@click="onClickNewPasskey()" @click="onClickNewPasskey()"
class="block w-full text-center text-lg uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md mb-2 cursor-pointer" class="block w-full text-center text-lg uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md mb-2 cursor-pointer"
> >
@ -88,7 +89,7 @@
<script lang="ts"> <script lang="ts">
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { AppString } from "@/constants/app"; import { AppString, PASSKEYS_ENABLED } from "@/constants/app";
import { accountsDB, db } from "@/db/index"; import { accountsDB, db } from "@/db/index";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { registerSaveAndActivatePasskey } from "@/libs/util"; import { registerSaveAndActivatePasskey } from "@/libs/util";
@ -97,6 +98,8 @@ import { registerSaveAndActivatePasskey } from "@/libs/util";
components: {}, components: {},
}) })
export default class StartView extends Vue { export default class StartView extends Vue {
PASSKEYS_ENABLED = PASSKEYS_ENABLED;
givenName = ""; givenName = "";
numAccounts = 0; numAccounts = 0;

Loading…
Cancel
Save