forked from jsnbuchanan/crowd-funder-for-time-pwa
make a passkey-generator in start & home pages, and make that the default
This commit is contained in:
@@ -20,6 +20,7 @@ import {
|
|||||||
PublicKeyCredentialRequestOptionsJSON,
|
PublicKeyCredentialRequestOptionsJSON,
|
||||||
} from "@simplewebauthn/types";
|
} from "@simplewebauthn/types";
|
||||||
|
|
||||||
|
import { AppString } from "@/constants/app";
|
||||||
import { getWebCrypto, unwrapEC2Signature } from "@/libs/crypto/passkeyHelpers";
|
import { getWebCrypto, unwrapEC2Signature } from "@/libs/crypto/passkeyHelpers";
|
||||||
|
|
||||||
const PEER_DID_PREFIX = "did:peer:";
|
const PEER_DID_PREFIX = "did:peer:";
|
||||||
@@ -42,9 +43,9 @@ function arrayToBase64Url(anything: Uint8Array) {
|
|||||||
export async function registerCredential(passkeyName?: string) {
|
export async function registerCredential(passkeyName?: string) {
|
||||||
const options: PublicKeyCredentialCreationOptionsJSON =
|
const options: PublicKeyCredentialCreationOptionsJSON =
|
||||||
await generateRegistrationOptions({
|
await generateRegistrationOptions({
|
||||||
rpName: "Time Safari",
|
rpName: AppString.APP_NAME,
|
||||||
rpID: window.location.hostname,
|
rpID: window.location.hostname,
|
||||||
userName: passkeyName || "Time Safari User",
|
userName: passkeyName || AppString.APP_NAME + " User",
|
||||||
// Don't prompt users for additional information about the authenticator
|
// Don't prompt users for additional information about the authenticator
|
||||||
// (Recommended for smoother UX)
|
// (Recommended for smoother UX)
|
||||||
attestationType: "none",
|
attestationType: "none",
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ import { 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 { createPeerDid, registerCredential } from "@/libs/didPeer";
|
||||||
|
|
||||||
|
import { Buffer } from "buffer";
|
||||||
|
|
||||||
export const PRIVACY_MESSAGE =
|
export const PRIVACY_MESSAGE =
|
||||||
"The data you send be visible to the world -- except: your IDs and the IDs of anyone you tag will stay private, only visible to those you allow.";
|
"The data you send be visible to the world -- except: your IDs and the IDs of anyone you tag will stay private, only visible to those you allow.";
|
||||||
@@ -239,6 +242,38 @@ export const generateSaveAndActivateIdentity = async (): Promise<string> => {
|
|||||||
return newId.did;
|
return newId.did;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const registerAndSavePasskey = async (
|
||||||
|
keyName: string,
|
||||||
|
): Promise<Account> => {
|
||||||
|
const cred = await registerCredential(keyName);
|
||||||
|
const publicKeyBytes = cred.publicKeyBytes;
|
||||||
|
const did = createPeerDid(publicKeyBytes as Uint8Array);
|
||||||
|
const passkeyCredIdHex = cred.credIdHex as string;
|
||||||
|
|
||||||
|
const account = {
|
||||||
|
dateCreated: new Date().toISOString(),
|
||||||
|
did,
|
||||||
|
passkeyCredIdHex,
|
||||||
|
publicKeyHex: Buffer.from(publicKeyBytes).toString("hex"),
|
||||||
|
};
|
||||||
|
await accountsDB.open();
|
||||||
|
await accountsDB.accounts.add(account);
|
||||||
|
return account;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const registerSaveAndActivatePasskey = async (
|
||||||
|
keyName: string,
|
||||||
|
): Promise<Account> => {
|
||||||
|
const account = await registerAndSavePasskey(keyName);
|
||||||
|
|
||||||
|
await db.open();
|
||||||
|
await db.settings.update(MASTER_SETTINGS_KEY, {
|
||||||
|
activeDid: account.did,
|
||||||
|
});
|
||||||
|
|
||||||
|
return account;
|
||||||
|
};
|
||||||
|
|
||||||
export const sendTestThroughPushServer = async (
|
export const sendTestThroughPushServer = async (
|
||||||
subscriptionJSON: PushSubscriptionJSON,
|
subscriptionJSON: PushSubscriptionJSON,
|
||||||
skipFilter: boolean,
|
skipFilter: boolean,
|
||||||
|
|||||||
@@ -180,16 +180,17 @@ 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 { 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, Settings } from "@/db/tables/settings";
|
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
|
||||||
import {
|
import {
|
||||||
constructGive,
|
constructGive,
|
||||||
createAndSubmitGive, didInfo,
|
createAndSubmitGive,
|
||||||
|
didInfo,
|
||||||
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 { accessToken } from "@/libs/crypto";
|
||||||
import {Contact} from "@/db/tables/contacts";
|
import { Contact } from "@/db/tables/contacts";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<!-- CONTENT -->
|
<!-- CONTENT -->
|
||||||
<section id="Content" class="p-2 pb-24 max-w-3xl mx-auto">
|
<section id="Content" class="p-2 pb-24 max-w-3xl mx-auto">
|
||||||
<h1 id="ViewHeading" class="text-4xl text-center font-light px-4 mb-8">
|
<h1 id="ViewHeading" class="text-4xl text-center font-light px-4 mb-8">
|
||||||
Time Safari
|
{{ AppString.APP_NAME }}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<!-- prompt to install notifications -->
|
<!-- prompt to install notifications -->
|
||||||
@@ -79,89 +79,100 @@
|
|||||||
<!-- !isCreatingIdentifier -->
|
<!-- !isCreatingIdentifier -->
|
||||||
<div
|
<div
|
||||||
v-if="!activeDid"
|
v-if="!activeDid"
|
||||||
class="bg-amber-200 rounded-md overflow-hidden text-center px-4 py-3 mb-4"
|
class="bg-amber-200 rounded-md text-center px-4 py-3 mb-4"
|
||||||
>
|
>
|
||||||
<p class="text-lg mb-3">
|
<p class="text-lg mb-3">
|
||||||
Want to connect with your contacts, or share contributions or
|
Want to see info from your contacts, or share contributions?
|
||||||
projects?
|
|
||||||
</p>
|
</p>
|
||||||
<router-link
|
<div class="flex justify-between">
|
||||||
:to="{ name: 'start' }"
|
<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"
|
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()"
|
||||||
Create An Identifier
|
>
|
||||||
</router-link>
|
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>
|
||||||
|
|
||||||
<div
|
<div v-else class="mb-4">
|
||||||
v-else-if="!isRegistered"
|
<!-- activeDid -->
|
||||||
class="bg-amber-200 rounded-md overflow-hidden text-center px-4 py-3 mb-4"
|
|
||||||
>
|
<div
|
||||||
<!-- activeDid && !isRegistered -->
|
v-if="!isRegistered"
|
||||||
Someone must register you before you can give kudos or make offers or
|
class="bg-amber-200 rounded-md overflow-hidden text-center px-4 py-3 mb-4"
|
||||||
create projects... basically before doing anything.
|
|
||||||
<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
|
<!-- activeDid && !isRegistered -->
|
||||||
</router-link>
|
Someone must register you before you can give kudos or make offers
|
||||||
</div>
|
or create projects... basically before doing anything.
|
||||||
|
<router-link
|
||||||
<div v-else>
|
:to="{ name: 'contact-qr' }"
|
||||||
<!-- activeDid && isRegistered -->
|
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 the actions for recognizing a give -->
|
Show Them Your Identifier Info
|
||||||
<div class="mb-4">
|
</router-link>
|
||||||
<h2 class="text-xl font-bold">Record Something Given By:</h2>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul
|
<div v-else>
|
||||||
class="grid grid-cols-4 sm:grid-cols-5 md:grid-cols-6 gap-x-3 gap-y-5 text-center mb-5"
|
<!-- activeDid && isRegistered -->
|
||||||
>
|
|
||||||
<li @click="openDialog()">
|
|
||||||
<img
|
|
||||||
src="../assets/blank-square.svg"
|
|
||||||
class="mx-auto border border-slate-300 rounded-md mb-1"
|
|
||||||
/>
|
|
||||||
<h3
|
|
||||||
class="text-xs italic font-medium text-ellipsis whitespace-nowrap overflow-hidden"
|
|
||||||
>
|
|
||||||
Unnamed/Unknown
|
|
||||||
</h3>
|
|
||||||
</li>
|
|
||||||
<li
|
|
||||||
v-for="contact in allContacts.slice(0, 7)"
|
|
||||||
:key="contact.did"
|
|
||||||
@click="openDialog(contact)"
|
|
||||||
>
|
|
||||||
<EntityIcon
|
|
||||||
:contact="contact"
|
|
||||||
:iconSize="64"
|
|
||||||
class="mx-auto border border-slate-300 rounded-md mb-1 cursor-pointer"
|
|
||||||
/>
|
|
||||||
<h3
|
|
||||||
class="text-xs font-medium text-ellipsis whitespace-nowrap overflow-hidden"
|
|
||||||
>
|
|
||||||
{{ contact.name || contact.did }}
|
|
||||||
</h3>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<div class="flex justify-between">
|
<!-- show the actions for recognizing a give -->
|
||||||
<router-link
|
<div class="mb-4">
|
||||||
v-if="allContacts.length >= 7"
|
<h2 class="text-xl font-bold">Record Something Given By:</h2>
|
||||||
:to="{ name: 'contact-gift' }"
|
</div>
|
||||||
class="block text-center text-md font-bold 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-2 py-3 rounded-md"
|
|
||||||
|
<ul
|
||||||
|
class="grid grid-cols-4 sm:grid-cols-5 md:grid-cols-6 gap-x-3 gap-y-5 text-center mb-5"
|
||||||
>
|
>
|
||||||
Choose From All Contacts
|
<li @click="openDialog()">
|
||||||
</router-link>
|
<img
|
||||||
<button
|
src="../assets/blank-square.svg"
|
||||||
@click="openGiftedPrompts()"
|
class="mx-auto border border-slate-300 rounded-md mb-1"
|
||||||
class="block text-center text-md 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-4 py-2 rounded-md"
|
/>
|
||||||
>
|
<h3
|
||||||
Ideas...
|
class="text-xs italic font-medium text-ellipsis whitespace-nowrap overflow-hidden"
|
||||||
</button>
|
>
|
||||||
|
Unnamed/Unknown
|
||||||
|
</h3>
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
v-for="contact in allContacts.slice(0, 7)"
|
||||||
|
:key="contact.did"
|
||||||
|
@click="openDialog(contact)"
|
||||||
|
>
|
||||||
|
<EntityIcon
|
||||||
|
:contact="contact"
|
||||||
|
:iconSize="64"
|
||||||
|
class="mx-auto border border-slate-300 rounded-md mb-1 cursor-pointer"
|
||||||
|
/>
|
||||||
|
<h3
|
||||||
|
class="text-xs font-medium text-ellipsis whitespace-nowrap overflow-hidden"
|
||||||
|
>
|
||||||
|
{{ contact.name || contact.did }}
|
||||||
|
</h3>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<router-link
|
||||||
|
v-if="allContacts.length >= 7"
|
||||||
|
:to="{ name: 'contact-gift' }"
|
||||||
|
class="block text-center text-md font-bold 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-2 py-3 rounded-md"
|
||||||
|
>
|
||||||
|
Choose From All Contacts
|
||||||
|
</router-link>
|
||||||
|
<button
|
||||||
|
@click="openGiftedPrompts()"
|
||||||
|
class="block text-center text-md 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-4 py-2 rounded-md"
|
||||||
|
>
|
||||||
|
Ideas...
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -309,6 +320,7 @@ import { IIdentifier } from "@veramo/core";
|
|||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import { Router } from "vue-router";
|
import { Router } from "vue-router";
|
||||||
|
|
||||||
|
import App from "../App.vue";
|
||||||
import EntityIcon from "@/components/EntityIcon.vue";
|
import EntityIcon from "@/components/EntityIcon.vue";
|
||||||
import GiftedDialog from "@/components/GiftedDialog.vue";
|
import GiftedDialog from "@/components/GiftedDialog.vue";
|
||||||
import GiftedPrompts from "@/components/GiftedPrompts.vue";
|
import GiftedPrompts from "@/components/GiftedPrompts.vue";
|
||||||
@@ -316,7 +328,7 @@ 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 { NotificationIface } from "@/constants/app";
|
import { AppString, NotificationIface } 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 { Contact } from "@/db/tables/contacts";
|
import { Contact } from "@/db/tables/contacts";
|
||||||
@@ -336,7 +348,7 @@ import {
|
|||||||
GiverReceiverInputInfo,
|
GiverReceiverInputInfo,
|
||||||
GiveSummaryRecord,
|
GiveSummaryRecord,
|
||||||
} from "@/libs/endorserServer";
|
} from "@/libs/endorserServer";
|
||||||
import { generateSaveAndActivateIdentity } from "@/libs/util";
|
import { registerSaveAndActivatePasskey } from "@/libs/util";
|
||||||
|
|
||||||
interface GiveRecordWithContactInfo extends GiveSummaryRecord {
|
interface GiveRecordWithContactInfo extends GiveSummaryRecord {
|
||||||
giver: {
|
giver: {
|
||||||
@@ -354,6 +366,11 @@ interface GiveRecordWithContactInfo extends GiveSummaryRecord {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
computed: {
|
||||||
|
App() {
|
||||||
|
return App;
|
||||||
|
},
|
||||||
|
},
|
||||||
components: {
|
components: {
|
||||||
GiftedDialog,
|
GiftedDialog,
|
||||||
GiftedPrompts,
|
GiftedPrompts,
|
||||||
@@ -367,6 +384,8 @@ interface GiveRecordWithContactInfo extends GiveSummaryRecord {
|
|||||||
export default class HomeView extends Vue {
|
export default class HomeView extends Vue {
|
||||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||||
|
|
||||||
|
AppString = AppString;
|
||||||
|
|
||||||
activeDid = "";
|
activeDid = "";
|
||||||
allContacts: Array<Contact> = [];
|
allContacts: Array<Contact> = [];
|
||||||
allMyDids: Array<string> = [];
|
allMyDids: Array<string> = [];
|
||||||
@@ -374,6 +393,7 @@ export default class HomeView extends Vue {
|
|||||||
feedData: GiveRecordWithContactInfo[] = [];
|
feedData: GiveRecordWithContactInfo[] = [];
|
||||||
feedPreviousOldestId?: string;
|
feedPreviousOldestId?: string;
|
||||||
feedLastViewedClaimId?: string;
|
feedLastViewedClaimId?: string;
|
||||||
|
givenName = "";
|
||||||
isAnyFeedFilterOn: boolean;
|
isAnyFeedFilterOn: boolean;
|
||||||
isCreatingIdentifier = false;
|
isCreatingIdentifier = false;
|
||||||
isFeedFilteredByVisible = false;
|
isFeedFilteredByVisible = false;
|
||||||
@@ -397,15 +417,6 @@ export default class HomeView extends Vue {
|
|||||||
return identity; // may be null
|
return identity; // may be null
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getHeaders(identity: IIdentifier) {
|
|
||||||
const token = await accessToken(identity);
|
|
||||||
const headers = {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: "Bearer " + token,
|
|
||||||
};
|
|
||||||
return headers;
|
|
||||||
}
|
|
||||||
|
|
||||||
async mounted() {
|
async mounted() {
|
||||||
try {
|
try {
|
||||||
await accountsDB.open();
|
await accountsDB.open();
|
||||||
@@ -418,6 +429,7 @@ export default class HomeView extends Vue {
|
|||||||
this.activeDid = settings?.activeDid || "";
|
this.activeDid = settings?.activeDid || "";
|
||||||
this.allContacts = await db.contacts.toArray();
|
this.allContacts = await db.contacts.toArray();
|
||||||
this.feedLastViewedClaimId = settings?.lastViewedClaimId;
|
this.feedLastViewedClaimId = settings?.lastViewedClaimId;
|
||||||
|
this.givenName = settings?.firstName || "";
|
||||||
this.isFeedFilteredByVisible = !!settings?.filterFeedByVisible;
|
this.isFeedFilteredByVisible = !!settings?.filterFeedByVisible;
|
||||||
this.isFeedFilteredByNearby = !!settings?.filterFeedByNearby;
|
this.isFeedFilteredByNearby = !!settings?.filterFeedByNearby;
|
||||||
this.isRegistered = !!settings?.isRegistered;
|
this.isRegistered = !!settings?.isRegistered;
|
||||||
@@ -426,14 +438,7 @@ export default class HomeView extends Vue {
|
|||||||
|
|
||||||
this.isAnyFeedFilterOn = isAnyFeedFilterOn(settings);
|
this.isAnyFeedFilterOn = isAnyFeedFilterOn(settings);
|
||||||
|
|
||||||
if (this.allMyDids.length === 0) {
|
// someone may have have registered after sharing contact info, so recheck
|
||||||
this.isCreatingIdentifier = true;
|
|
||||||
this.activeDid = await generateSaveAndActivateIdentity();
|
|
||||||
this.allMyDids = [this.activeDid];
|
|
||||||
this.isCreatingIdentifier = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// someone may have have registered after sharing contact info
|
|
||||||
if (!this.isRegistered && this.activeDid) {
|
if (!this.isRegistered && this.activeDid) {
|
||||||
const identity = await this.getIdentity(this.activeDid);
|
const identity = await this.getIdentity(this.activeDid);
|
||||||
try {
|
try {
|
||||||
@@ -475,6 +480,15 @@ export default class HomeView extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async generateIdentifier() {
|
||||||
|
this.isCreatingIdentifier = true;
|
||||||
|
const account = await registerSaveAndActivatePasskey(
|
||||||
|
AppString.APP_NAME + (this.givenName ? " - " + this.givenName : ""),
|
||||||
|
);
|
||||||
|
this.activeDid = account.did;
|
||||||
|
this.allMyDids = this.allMyDids.concat(this.activeDid);
|
||||||
|
this.isCreatingIdentifier = false;
|
||||||
|
}
|
||||||
resultsAreFiltered() {
|
resultsAreFiltered() {
|
||||||
return this.isFeedFilteredByVisible || this.isFeedFilteredByNearby;
|
return this.isFeedFilteredByVisible || this.isFeedFilteredByNearby;
|
||||||
}
|
}
|
||||||
@@ -483,7 +497,7 @@ export default class HomeView extends Vue {
|
|||||||
return "Notification" in window;
|
return "Notification" in window;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async buildHeaders() {
|
async buildHeaders() {
|
||||||
const headers: HeadersInit = {
|
const headers: HeadersInit = {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
};
|
};
|
||||||
@@ -520,7 +534,7 @@ export default class HomeView extends Vue {
|
|||||||
* Data loader used by infinite scroller
|
* Data loader used by infinite scroller
|
||||||
* @param payload is the flag from the InfiniteScroll indicating if it should load
|
* @param payload is the flag from the InfiniteScroll indicating if it should load
|
||||||
**/
|
**/
|
||||||
public async loadMoreGives(payload: boolean) {
|
async loadMoreGives(payload: boolean) {
|
||||||
// Since feed now loads projects along the way, it takes longer
|
// Since feed now loads projects along the way, it takes longer
|
||||||
// and the InfiniteScroll component triggers a load before finished.
|
// and the InfiniteScroll component triggers a load before finished.
|
||||||
// One alternative is to totally separate the project link loading.
|
// One alternative is to totally separate the project link loading.
|
||||||
@@ -542,7 +556,7 @@ export default class HomeView extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async updateAllFeed() {
|
async updateAllFeed() {
|
||||||
this.isFeedLoading = true;
|
this.isFeedLoading = true;
|
||||||
let endOfResults = true;
|
let endOfResults = true;
|
||||||
await this.retrieveGives(this.apiServer, this.feedPreviousOldestId)
|
await this.retrieveGives(this.apiServer, this.feedPreviousOldestId)
|
||||||
@@ -650,7 +664,7 @@ export default class HomeView extends Vue {
|
|||||||
* @param beforeId the earliest ID (of previous searches) to search earlier
|
* @param beforeId the earliest ID (of previous searches) to search earlier
|
||||||
* @return claims in reverse chronological order
|
* @return claims in reverse chronological order
|
||||||
*/
|
*/
|
||||||
public async retrieveGives(endorserApiServer: string, beforeId?: string) {
|
async retrieveGives(endorserApiServer: string, beforeId?: string) {
|
||||||
const beforeQuery = beforeId == null ? "" : "&beforeId=" + beforeId;
|
const beforeQuery = beforeId == null ? "" : "&beforeId=" + beforeId;
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
endorserApiServer +
|
endorserApiServer +
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
|
|
||||||
<!-- Heading -->
|
<!-- Heading -->
|
||||||
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
|
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
|
||||||
Start Here
|
Generate an Identity
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -25,33 +25,57 @@
|
|||||||
<div id="start-question" class="mt-8">
|
<div id="start-question" class="mt-8">
|
||||||
<div class="max-w-3xl mx-auto">
|
<div class="max-w-3xl mx-auto">
|
||||||
<p class="text-center text-xl font-light">
|
<p class="text-center text-xl font-light">
|
||||||
Do you want a new identifier of your own?
|
How do you want to create this identifier?
|
||||||
</p>
|
</p>
|
||||||
<p class="text-center font-light">
|
<p class="text-center font-light mt-6">
|
||||||
If you haven't used this before, click "Yes" to generate a new
|
A <strong>passkey</strong> is easy to manage, though it is less
|
||||||
identifier.
|
interoperable with other systems for advanced uses.
|
||||||
|
<a
|
||||||
|
href="https://www.perplexity.ai/search/what-are-passkeys-v2SHV3yLQlyA2CYH6.Nvhg"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<fa icon="info-circle" class="fa-fw text-blue-500" />
|
||||||
|
</a>
|
||||||
</p>
|
</p>
|
||||||
<p class="text-center mb-4 font-light">
|
<p class="text-center font-light mt-4">
|
||||||
Only click "No" if you have a seed of 12 or 24 words generated
|
A <strong>new seed</strong> allows you full control over the keys,
|
||||||
elsewhere.
|
though you are responsible for backups.
|
||||||
|
<a
|
||||||
|
href="https://www.perplexity.ai/search/what-is-a-seed-phrase-OqiP9foVRXidr_2le5OFKA"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<fa icon="info-circle" class="fa-fw text-blue-500" />
|
||||||
|
</a>
|
||||||
</p>
|
</p>
|
||||||
<a
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2 mt-4">
|
||||||
@click="onClickYes()"
|
<a
|
||||||
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"
|
@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"
|
||||||
Yes, generate one
|
>
|
||||||
</a>
|
Generate one with a passkey
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
</a>
|
||||||
|
<a
|
||||||
|
@click="onClickNewSeed()"
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
Generate one with a new seed
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<p class="text-center font-light mt-4">
|
||||||
|
You can also import an existing seed or derive a new address from an
|
||||||
|
existing seed.
|
||||||
|
</p>
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2 mt-2">
|
||||||
<a
|
<a
|
||||||
@click="onClickNo()"
|
@click="onClickNo()"
|
||||||
class="block w-full text-center text-md uppercase 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"
|
class="block w-full text-center text-md 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-1.5 py-2 rounded-md cursor-pointer"
|
||||||
>
|
>
|
||||||
No, I have a seed
|
You have a seed
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
v-if="numAccounts > 0"
|
v-if="numAccounts > 0"
|
||||||
@click="onClickDerive()"
|
@click="onClickDerive()"
|
||||||
class="block w-full text-center text-md uppercase 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"
|
class="block w-full text-center text-md 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-1.5 py-2 rounded-md cursor-pointer"
|
||||||
>
|
>
|
||||||
Derive new address from existing seed
|
Derive new address from existing seed
|
||||||
</a>
|
</a>
|
||||||
@@ -64,23 +88,38 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
|
|
||||||
import { accountsDB } from "@/db/index";
|
import { AppString } from "@/constants/app";
|
||||||
|
import { accountsDB, db } from "@/db/index";
|
||||||
|
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
|
||||||
|
import { registerSaveAndActivatePasskey } from "@/libs/util";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {},
|
components: {},
|
||||||
})
|
})
|
||||||
export default class StartView extends Vue {
|
export default class StartView extends Vue {
|
||||||
|
givenName = "";
|
||||||
numAccounts = 0;
|
numAccounts = 0;
|
||||||
|
|
||||||
async mounted() {
|
async mounted() {
|
||||||
|
await db.open();
|
||||||
|
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
|
||||||
|
this.givenName = settings?.firstName || "";
|
||||||
|
|
||||||
await accountsDB.open();
|
await accountsDB.open();
|
||||||
this.numAccounts = await accountsDB.accounts.count();
|
this.numAccounts = await accountsDB.accounts.count();
|
||||||
}
|
}
|
||||||
|
|
||||||
public onClickYes() {
|
public onClickNewSeed() {
|
||||||
this.$router.push({ name: "new-identifier" });
|
this.$router.push({ name: "new-identifier" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async onClickNewPasskey() {
|
||||||
|
const keyName =
|
||||||
|
AppString.APP_NAME + (this.givenName ? " - " + this.givenName : "");
|
||||||
|
await registerSaveAndActivatePasskey(keyName);
|
||||||
|
this.$router.push({ name: "account" });
|
||||||
|
}
|
||||||
|
|
||||||
public onClickNo() {
|
public onClickNo() {
|
||||||
this.$router.push({ name: "import-account" });
|
this.$router.push({ name: "import-account" });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -244,7 +244,7 @@ import { ref } from "vue";
|
|||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
|
|
||||||
import QuickNav from "@/components/QuickNav.vue";
|
import QuickNav from "@/components/QuickNav.vue";
|
||||||
import { NotificationIface } from "@/constants/app";
|
import { AppString, NotificationIface } from "@/constants/app";
|
||||||
import { accountsDB, db } from "@/db/index";
|
import { accountsDB, db } from "@/db/index";
|
||||||
import {
|
import {
|
||||||
createPeerDid,
|
createPeerDid,
|
||||||
@@ -255,6 +255,7 @@ import {
|
|||||||
verifyJwtWebCrypto,
|
verifyJwtWebCrypto,
|
||||||
} from "@/libs/didPeer";
|
} from "@/libs/didPeer";
|
||||||
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||||
|
import {registerAndSavePasskey} from "@/libs/util";
|
||||||
|
|
||||||
const inputFileNameRef = ref<Blob>();
|
const inputFileNameRef = ref<Blob>();
|
||||||
|
|
||||||
@@ -333,14 +334,14 @@ export default class Help extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async register() {
|
public async register() {
|
||||||
const DEFAULT_USERNAME = "Time Safari Tester";
|
const DEFAULT_USERNAME = AppString.APP_NAME + " Tester";
|
||||||
if (!this.userName) {
|
if (!this.userName) {
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "modal",
|
group: "modal",
|
||||||
type: "confirm",
|
type: "confirm",
|
||||||
title: "No Name",
|
title: "No Name",
|
||||||
text: "You must have a name to attach to this passkey. Would you like to enter your own name first?",
|
text: "You should have a name to attach to this passkey. Would you like to enter your own name first?",
|
||||||
onNo: async () => {
|
onNo: async () => {
|
||||||
this.userName = DEFAULT_USERNAME;
|
this.userName = DEFAULT_USERNAME;
|
||||||
},
|
},
|
||||||
@@ -353,18 +354,11 @@ export default class Help extends Vue {
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const cred = await registerCredential("Time Safari - " + this.userName);
|
const account = await registerAndSavePasskey(
|
||||||
const publicKeyBytes = cred.publicKeyBytes;
|
AppString.APP_NAME + " - " + this.userName,
|
||||||
this.activeDid = createPeerDid(publicKeyBytes as Uint8Array);
|
);
|
||||||
this.credIdHex = cred.credIdHex as string;
|
this.activeDid = account.did;
|
||||||
|
this.credIdHex = account.passkeyCredIdHex;
|
||||||
await accountsDB.open();
|
|
||||||
await accountsDB.accounts.add({
|
|
||||||
dateCreated: new Date().toISOString(),
|
|
||||||
did: this.activeDid,
|
|
||||||
passkeyCredIdHex: this.credIdHex,
|
|
||||||
publicKeyHex: Buffer.from(publicKeyBytes).toString("hex"),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async createJwtSimplewebauthn() {
|
public async createJwtSimplewebauthn() {
|
||||||
|
|||||||
Reference in New Issue
Block a user