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
lastNotifiedClaimId?: string;
lastViewedClaimId?: string;
passkeyExpirationMinutes?: number; // passkey access token time-to-live in minutes
profileImageUrl?: string;
reminderTime?: number; // Time in milliseconds since UNIX epoch for reminders
reminderOn?: boolean; // Toggle to enable or disable reminders
@ -46,7 +47,7 @@ export type Settings = {
};
export function isAnyFeedFilterOn(settings: Settings): boolean {
return !!(settings.filterFeedByNearby || settings.filterFeedByVisible);
return !!(settings?.filterFeedByNearby || settings?.filterFeedByVisible);
}
/**
@ -60,3 +61,5 @@ export const SettingsSchema = {
* Constants.
*/
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 {*}
*/

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
// (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 multibase = id.slice(1);
const encnumbasis = multibase.slice(1);
const didDocument = {
"@context": [
"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],
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 { Contact } from "@/db/tables/contacts";
import { accessToken } from "@/libs/crypto";
import { NonsensitiveDexie } from "@/db/index";
import { getAccount } from "@/libs/util";
import { db, NonsensitiveDexie } from "@/db/index";
import { getAccount, getPasskeyExpirationSeconds } from "@/libs/util";
import { createEndorserJwtForKey, KeyMeta } from "@/libs/crypto/vc";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
export const SCHEMA_ORG_CONTEXT = "https://schema.org";
// the object in RegisterAction claims
@ -447,12 +448,57 @@ export function didInfo(
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) {
const headers: { "Content-Type": string; Authorization?: string } = {
"Content-Type": "application/json",
};
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;
} else {
// 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
import axios, { AxiosResponse } from "axios";
import { IIdentifier } from "@veramo/core";
import { useClipboard } from "@vueuse/core";
import { DEFAULT_PUSH_SERVER } from "@/constants/app";
import { accountsDB, db } from "@/db/index";
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 { GenericCredWrapper, containsHiddenDid } from "@/libs/endorserServer";
import * as serverUtil from "@/libs/endorserServer";
import { registerCredential } from "@/libs/crypto/vc/passkeyDidPeer";
import { Buffer } from "buffer";
import {KeyMeta} from "@/libs/crypto/vc";
import {createPeerDid} from "@/libs/crypto/vc/didPeer";
import { KeyMeta } from "@/libs/crypto/vc";
import { createPeerDid } from "@/libs/crypto/vc/didPeer";
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.";
@ -273,6 +272,15 @@ export const registerSaveAndActivatePasskey = async (
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 (
subscriptionJSON: PushSubscriptionJSON,
skipFilter: boolean,

250
src/views/AccountViewView.vue

@ -152,7 +152,7 @@
<div class="text-blue-500 text-sm font-bold">
<router-link :to="{ path: '/did/' + encodeURIComponent(activeDid) }">
Activity
Your Activity
</router-link>
</div>
</div>
@ -216,7 +216,6 @@
<div class="mb-2 font-bold">Location</div>
<router-link
: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"
>
Set Search Area
@ -622,6 +621,26 @@
</button>
</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
for="toggleShowGeneralAdvanced"
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 TopMessage from "@/components/TopMessage.vue";
import {
AppString,
AppString, DEFAULT_ENDORSER_API_SERVER,
DEFAULT_IMAGE_API_SERVER,
DEFAULT_PUSH_SERVER,
IMAGE_TYPE_PROFILE,
@ -675,14 +694,20 @@ import {
} from "@/constants/app";
import { db, accountsDB } from "@/db/index";
import { Account } from "@/db/tables/accounts";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";
import {
DEFAULT_PASSKEY_EXPIRATION_MINUTES,
MASTER_SETTINGS_KEY,
Settings,
} from "@/db/tables/settings";
import {
clearPasskeyToken,
ErrorResponse,
EndorserRateLimits,
ImageRateLimits,
fetchEndorserRateLimits,
fetchImageRateLimits,
getHeaders,
ImageRateLimits,
tokenExpiryTimeDescription,
} from "@/libs/endorserServer";
import { getAccount } from "@/libs/util";
@ -713,6 +738,9 @@ export default class AccountViewView extends Vue {
limitsMessage = "";
loadingLimits = false;
notificationMaybeChanged = false;
passkeyExpirationDescription = "";
passkeyExpirationMinutes = DEFAULT_PASSKEY_EXPIRATION_MINUTES;
previousPasskeyExpirationMinutes = DEFAULT_PASSKEY_EXPIRATION_MINUTES;
profileImageUrl?: string;
publicHex = "";
publicBase64 = "";
@ -745,12 +773,32 @@ export default class AccountViewView extends Vue {
await this.initializeState();
await this.processIdentity();
this.passkeyExpirationDescription = tokenExpiryTimeDescription();
/**
* Beware! I've seen where this "ready" never resolves.
*/
const registration = await navigator.serviceWorker.ready;
this.subscription = await registration.pushManager.getSubscription();
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) {
console.error("Mount error:", error);
this.handleError(error);
console.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.hideRegisterPromptOnNewContact =
!!settings?.hideRegisterPromptOnNewContact;
this.passkeyExpirationMinutes =
(settings?.passkeyExpirationMinutes as number) ??
DEFAULT_PASSKEY_EXPIRATION_MINUTES;
this.previousPasskeyExpirationMinutes = this.passkeyExpirationMinutes;
this.showGeneralAdvanced = !!settings?.showGeneralAdvanced;
this.showShortcutBvc = !!settings?.showShortcutBvc;
this.warnIfProdServer = !!settings?.warnIfProdServer;
@ -835,11 +887,11 @@ export default class AccountViewView extends Vue {
this.publicHex = identity.keys[0].publicKeyHex;
this.publicBase64 = Buffer.from(this.publicHex, "hex").toString("base64");
this.derivationPath = identity.keys[0].meta?.derivationPath as string;
this.checkLimitsFor(this.activeDid);
await this.checkLimitsFor(this.activeDid);
} else if (account?.publicKeyHex) {
this.publicHex = account.publicKeyHex as string;
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;
}
/**
* 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() {
try {
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, {
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,
);
}
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, {
showContactGivesInline: this.showContactGives,
});
}
public async updateShowGeneralAdvanced() {
try {
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, {
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,
);
}
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, {
showGeneralAdvanced: this.showGeneralAdvanced,
});
}
public async updateWarnIfProdServer(newSetting: boolean) {
@ -963,71 +958,35 @@ export default class AccountViewView extends Vue {
}
public async updateWarnIfTestServer(newSetting: boolean) {
try {
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, {
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,
);
}
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, {
warnIfTestServer: newSetting,
});
}
public async toggleHideRegisterPromptOnNewContact() {
const newSetting = !this.hideRegisterPromptOnNewContact;
try {
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, {
hideRegisterPromptOnNewContact: newSetting,
});
this.hideRegisterPromptOnNewContact = newSetting;
} catch (err) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Updating Setting",
text: "The setting may not have saved. Try again, maybe after restarting the app.",
},
-1,
);
console.error("Telling user to try again because:", err);
}
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, {
hideRegisterPromptOnNewContact: newSetting,
});
this.hideRegisterPromptOnNewContact = newSetting;
}
public async updatePasskeyExpiration() {
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, {
passkeyExpirationMinutes: this.passkeyExpirationMinutes,
});
clearPasskeyToken();
this.passkeyExpirationDescription = tokenExpiryTimeDescription();
}
public async updateShowShortcutBvc(newSetting: boolean) {
try {
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, {
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,
);
}
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, {
showShortcutBvc: newSetting,
});
}
/**
@ -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
try {
await db.open();
db.settings.update(MASTER_SETTINGS_KEY, {
await db.settings.update(MASTER_SETTINGS_KEY, {
isRegistered: true,
});
this.isRegistered = true;
@ -1247,7 +1206,7 @@ export default class AccountViewView extends Vue {
try {
await db.open();
db.settings.update(MASTER_SETTINGS_KEY, {
await db.settings.update(MASTER_SETTINGS_KEY, {
isRegistered: false,
});
this.isRegistered = false;
@ -1272,8 +1231,8 @@ export default class AccountViewView extends Vue {
(data?.error?.message as string) || "Bad server response.";
console.error(
"Got bad response retrieving limits, which usually means user isn't registered.",
error,
);
//console.error(error);
} else {
this.limitsMessage = "Got an error retrieving limits.";
console.error("Got some error retrieving limits:", error);
@ -1350,7 +1309,7 @@ export default class AccountViewView extends Vue {
async onClickSaveApiServer() {
await db.open();
db.settings.update(MASTER_SETTINGS_KEY, {
await db.settings.update(MASTER_SETTINGS_KEY, {
apiServer: this.apiServerInput,
});
this.apiServer = this.apiServerInput;
@ -1358,7 +1317,7 @@ export default class AccountViewView extends Vue {
async onClickSavePushServer() {
await db.open();
db.settings.update(MASTER_SETTINGS_KEY, {
await db.settings.update(MASTER_SETTINGS_KEY, {
webPushServer: this.webPushServerInput,
});
this.webPushServer = this.webPushServerInput;
@ -1377,7 +1336,7 @@ export default class AccountViewView extends Vue {
(this.$refs.imageMethodDialog as ImageMethodDialog).open(
async (imgUrl) => {
await db.open();
db.settings.update(MASTER_SETTINGS_KEY, {
await db.settings.update(MASTER_SETTINGS_KEY, {
profileImageUrl: imgUrl,
});
this.profileImageUrl = imgUrl;
@ -1407,16 +1366,13 @@ export default class AccountViewView extends Vue {
return;
}
try {
const token = await accessToken(this.activeDid);
const headers = await getHeaders(this.activeDid);
this.passkeyExpirationDescription = tokenExpiryTimeDescription();
const response = await this.axios.delete(
DEFAULT_IMAGE_API_SERVER +
"/image/" +
encodeURIComponent(this.profileImageUrl),
{
headers: {
Authorization: `Bearer ${token}`,
},
},
{ headers },
);
if (response.status === 204) {
// don't bother with a notification
@ -1436,7 +1392,7 @@ export default class AccountViewView extends Vue {
}
await db.open();
db.settings.update(MASTER_SETTINGS_KEY, {
await db.settings.update(MASTER_SETTINGS_KEY, {
profileImageUrl: undefined,
});
@ -1448,7 +1404,7 @@ export default class AccountViewView extends Vue {
console.error("The image was already deleted:", error);
await db.open();
db.settings.update(MASTER_SETTINGS_KEY, {
await db.settings.update(MASTER_SETTINGS_KEY, {
profileImageUrl: undefined,
});

15
src/views/ContactAmountsView.vue

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

10
src/views/GiftedDetails.vue

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

9
src/views/HelpView.vue

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

77
src/views/HomeView.vue

@ -77,58 +77,28 @@
<div v-else>
<!-- !isCreatingIdentifier -->
<div
v-if="!activeDid"
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 -->
<!-- They should have an identifier, even if it's an auto-generated one that they'll never use. -->
<div class="mb-4">
<div
v-if="!isRegistered"
class="bg-amber-200 rounded-md overflow-hidden text-center px-4 py-3 mb-4"
>
<!-- activeDid && !isRegistered -->
Someone must register you before you can give kudos or make offers
or create projects... basically before doing anything.
To share, someone must register you.
<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"
>
Show Them Your Identifier Info
Show Them Default Identifier Info
</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 v-else>
@ -340,7 +310,11 @@ import FeedFilters from "@/components/FeedFilters.vue";
import InfiniteScroll from "@/components/InfiniteScroll.vue";
import QuickNav from "@/components/QuickNav.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 { Contact } from "@/db/tables/contacts";
import {
@ -359,7 +333,10 @@ import {
GiverReceiverInputInfo,
GiveSummaryRecord,
} from "@/libs/endorserServer";
import { registerSaveAndActivatePasskey } from "@/libs/util";
import {
generateSaveAndActivateIdentity,
registerSaveAndActivatePasskey,
} from "@/libs/util";
interface GiveRecordWithContactInfo extends GiveSummaryRecord {
giver: {
@ -423,7 +400,14 @@ export default class HomeView extends Vue {
try {
await accountsDB.open();
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();
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);
// someone may have have registered after sharing contact info, so recheck
if (!this.isRegistered && this.activeDid) {
try {
@ -481,7 +466,7 @@ export default class HomeView extends Vue {
}
}
async generateIdentifier() {
async generatePasskeyIdentifier() {
this.isCreatingIdentifier = true;
const account = await registerSaveAndActivatePasskey(
AppString.APP_NAME + (this.givenName ? " - " + this.givenName : ""),

24
src/views/NewEditProjectView.vue

@ -173,7 +173,7 @@
<script lang="ts">
import "leaflet/dist/leaflet.css";
import { AxiosError } from "axios";
import { AxiosError, AxiosRequestHeaders } from "axios";
import { DateTime } from "luxon";
import { Component, Vue } from "vue-facing-decorator";
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 { accountsDB, db } from "@/db/index";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";
import {
createEndorserJwtVcFromClaim,
getHeaders,
PlanVerifiableCredential,
} from "@/libs/endorserServer";
import { useAppStore } from "@/store/app";
@ -250,11 +250,7 @@ export default class NewEditProjectView extends Vue {
this.apiServer +
"/api/claim/byHandle/" +
encodeURIComponent(this.projectId);
const token = await accessToken(userDid);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
const headers = await getHeaders(userDid);
try {
const resp = await this.axios.get(url, { headers });
@ -309,16 +305,12 @@ export default class NewEditProjectView extends Vue {
return;
}
try {
const token = await accessToken(this.activeDid);
const headers = getHeaders(this.activeDid) as AxiosRequestHeaders;
const response = await this.axios.delete(
DEFAULT_IMAGE_API_SERVER +
"/image/" +
encodeURIComponent(this.imageUrl),
{
headers: {
Authorization: `Bearer ${token}`,
},
},
{ headers },
);
if (response.status === 204) {
// don't bother with a notification
@ -418,11 +410,7 @@ export default class NewEditProjectView extends Vue {
const payload = JSON.stringify({ jwtEncoded: vcJwt });
const url = this.apiServer + "/api/v2/claim";
const token = await accessToken(issuerDid);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
const headers = await getHeaders(issuerDid);
try {
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 { Contact } from "@/db/tables/contacts";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";
import * as libsUtil from "@/libs/util";
import {
BLANK_GENERIC_SERVER_RECORD,
@ -583,11 +582,6 @@ export default class ProjectViewView extends Vue {
this.loadPlanFulfillersTo();
// now load fulfilled-by, a single project
if (this.activeDid) {
const token = await accessToken(this.activeDid);
headers["Authorization"] = "Bearer " + token;
}
const fulfilledByUrl =
this.apiServer +
"/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 { accountsDB, db } from "@/db/index";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";
import * as libsUtil from "@/libs/util";
import InfiniteScroll from "@/components/InfiniteScroll.vue";
import QuickNav from "@/components/QuickNav.vue";
import ProjectIcon from "@/components/ProjectIcon.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";
@Component({
@ -293,13 +296,9 @@ export default class ProjectsView extends Vue {
* @param url the url used to fetch the data
* @param token Authorization token
**/
async projectDataLoader(url: string, token: string) {
const headers: { [key: string]: string } = {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
};
async projectDataLoader(url: string) {
try {
const headers = await getHeaders(this.activeDid);
this.isLoading = true;
const resp = await this.axios.get(url, { headers } as AxiosRequestConfig);
if (resp.status === 200 && resp.data.data) {
@ -353,8 +352,7 @@ export default class ProjectsView extends Vue {
**/
async loadProjects(activeDid?: string, urlExtra: string = "") {
const url = `${this.apiServer}/api/v2/report/plansByIssuer?${urlExtra}`;
const token: string = await accessToken(activeDid);
await this.projectDataLoader(url, token);
await this.projectDataLoader(url);
}
/**
@ -392,11 +390,8 @@ export default class ProjectsView extends Vue {
* @param url the url used to fetch the data
* @param token Authorization token
**/
async offerDataLoader(url: string, token: string) {
const headers: { [key: string]: string } = {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
};
async offerDataLoader(url: string) {
const headers = getHeaders(this.activeDid);
try {
this.isLoading = true;
@ -454,8 +449,7 @@ export default class ProjectsView extends Vue {
**/
async loadOffers(issuerDid?: string, urlExtra: string = "") {
const url = `${this.apiServer}/api/v2/report/offers?offeredByDid=${issuerDid}${urlExtra}`;
const token: string = await accessToken(issuerDid);
await this.offerDataLoader(url, token);
await this.offerDataLoader(url);
}
public computedOfferTabClassNames() {

7
src/views/SharedPhotoView.vue

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

7
src/views/StartView.vue

@ -27,7 +27,7 @@
<p class="text-center text-xl font-light">
How do you want to create this identifier?
</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
interoperable with other systems for advanced uses.
<a
@ -49,6 +49,7 @@
</p>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2 mt-4">
<a
v-if="PASSKEYS_ENABLED"
@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"
>
@ -88,7 +89,7 @@
<script lang="ts">
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 { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { registerSaveAndActivatePasskey } from "@/libs/util";
@ -97,6 +98,8 @@ import { registerSaveAndActivatePasskey } from "@/libs/util";
components: {},
})
export default class StartView extends Vue {
PASSKEYS_ENABLED = PASSKEYS_ENABLED;
givenName = "";
numAccounts = 0;

Loading…
Cancel
Save