You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
580 lines
18 KiB
580 lines
18 KiB
<template>
|
|
<!-- QUICK NAV -->
|
|
<nav id="QuickNav" class="fixed bottom-0 left-0 right-0 bg-slate-200 z-50">
|
|
<ul class="flex text-2xl p-2 gap-2">
|
|
<!-- Home Feed -->
|
|
<li class="basis-1/5 rounded-md text-slate-500">
|
|
<router-link :to="{ name: 'home' }" class="block text-center py-3 px-1">
|
|
<fa icon="house-chimney" class="fa-fw"></fa>
|
|
</router-link>
|
|
</li>
|
|
<!-- Search -->
|
|
<li class="basis-1/5 rounded-md text-slate-500">
|
|
<router-link
|
|
:to="{ name: 'discover' }"
|
|
class="block text-center py-3 px-1"
|
|
>
|
|
<fa icon="magnifying-glass" class="fa-fw"></fa>
|
|
</router-link>
|
|
</li>
|
|
<!-- Projects -->
|
|
<li class="basis-1/5 rounded-md text-slate-500">
|
|
<router-link
|
|
:to="{ name: 'projects' }"
|
|
class="block text-center py-3 px-1"
|
|
>
|
|
<fa icon="folder-open" class="fa-fw"></fa>
|
|
</router-link>
|
|
</li>
|
|
<!-- Contacts -->
|
|
<li class="basis-1/5 rounded-md text-slate-500">
|
|
<router-link
|
|
:to="{ name: 'contacts' }"
|
|
class="block text-center py-3 px-1"
|
|
>
|
|
<fa icon="users" class="fa-fw"></fa>
|
|
</router-link>
|
|
</li>
|
|
<!-- Profile -->
|
|
<li class="basis-1/5 rounded-md bg-slate-400 text-white">
|
|
<router-link
|
|
:to="{ name: 'account' }"
|
|
class="block text-center py-3 px-1"
|
|
>
|
|
<fa icon="circle-user" class="fa-fw"></fa>
|
|
</router-link>
|
|
</li>
|
|
</ul>
|
|
</nav>
|
|
|
|
<!-- CONTENT -->
|
|
<section id="Content" class="p-6 pb-24">
|
|
<!-- Heading -->
|
|
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
|
|
Your Identity
|
|
</h1>
|
|
|
|
<div class="flex justify-between py-2">
|
|
<span />
|
|
<span>
|
|
<router-link
|
|
:to="{ name: 'help' }"
|
|
class="text-xs uppercase bg-blue-500 text-white px-1.5 py-1 rounded-md ml-1"
|
|
>
|
|
Help
|
|
</router-link>
|
|
</span>
|
|
</div>
|
|
|
|
<!-- Friend referral requirement notice -->
|
|
<div
|
|
class="bg-amber-200 text-amber-900 border-amber-500 border-dashed border text-center rounded-md overflow-hidden px-4 py-3 mb-4"
|
|
>
|
|
<p class="mb-4">
|
|
<b>Important:</b> before you can create a new project or commit time to
|
|
one, you need a friend to register you.
|
|
</p>
|
|
<button
|
|
id="btnShowQR"
|
|
class="inline-block text-md uppercase bg-amber-600 text-white px-4 py-2 rounded-md"
|
|
>
|
|
Share Your ID
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Identity Details -->
|
|
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
|
|
<h2 class="text-xl font-semibold mb-2">{{ firstName }} {{ lastName }}</h2>
|
|
|
|
<div class="text-slate-500 text-sm font-bold">ID</div>
|
|
<div class="text-sm text-slate-500 flex justify-start items-center mb-1">
|
|
<code class="truncate">{{ activeDid }}</code>
|
|
<button
|
|
@click="
|
|
doCopyTwoSecRedo(activeDid, () => (showDidCopy = !showDidCopy))
|
|
"
|
|
class="ml-2"
|
|
>
|
|
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
|
|
</button>
|
|
<span v-show="showDidCopy">Copied!</span>
|
|
<span class="whitespace-nowrap ml-4">
|
|
<router-link
|
|
:to="{ name: 'contact-qr' }"
|
|
class="text-xs uppercase bg-slate-500 text-white px-1.5 py-1 rounded-md ml-1"
|
|
>
|
|
<fa icon="qrcode" class="fa-fw"></fa>
|
|
</router-link>
|
|
</span>
|
|
</div>
|
|
|
|
<div class="text-slate-500 text-sm font-bold">Public Key (base 64)</div>
|
|
<div class="text-sm text-slate-500 flex justify-start items-center mb-1">
|
|
<code class="truncate">{{ publicBase64 }}</code>
|
|
<button
|
|
@click="
|
|
doCopyTwoSecRedo(publicBase64, () => (showB64Copy = !showB64Copy))
|
|
"
|
|
class="ml-2"
|
|
>
|
|
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
|
|
</button>
|
|
<span v-show="showB64Copy">Copied!</span>
|
|
</div>
|
|
|
|
<div class="text-slate-500 text-sm font-bold">Public Key (hex)</div>
|
|
<div class="text-sm text-slate-500 flex justify-start items-center mb-1">
|
|
<code class="truncate">{{ publicHex }}</code>
|
|
<button
|
|
@click="
|
|
doCopyTwoSecRedo(publicHex, () => (showPubCopy = !showPubCopy))
|
|
"
|
|
class="ml-2"
|
|
>
|
|
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
|
|
</button>
|
|
<span v-show="showPubCopy">Copied!</span>
|
|
</div>
|
|
|
|
<div class="text-slate-500 text-sm font-bold">Derivation Path</div>
|
|
<div class="text-sm text-slate-500 flex justify-start items-center mb-1">
|
|
<code class="truncate">{{ derivationPath }}</code>
|
|
<button
|
|
@click="
|
|
doCopyTwoSecRedo(derivationPath, () => (showDerCopy = !showDerCopy))
|
|
"
|
|
class="ml-2"
|
|
>
|
|
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
|
|
</button>
|
|
<span v-show="showDerCopy">Copied!</span>
|
|
</div>
|
|
</div>
|
|
|
|
<router-link
|
|
:to="{ name: 'new-edit-account' }"
|
|
class="block text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md mb-8"
|
|
>
|
|
Edit Identity
|
|
</router-link>
|
|
|
|
<h3 class="text-sm uppercase font-semibold mb-3">Data</h3>
|
|
|
|
<router-link
|
|
:to="{ name: 'seed-backup' }"
|
|
href=""
|
|
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
|
|
>
|
|
Backup Identifier Seed
|
|
</router-link>
|
|
<a
|
|
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-6"
|
|
@click="exportDatabase()"
|
|
>
|
|
Download Settings & Contacts (excluding Identifier Data)
|
|
</a>
|
|
<a ref="downloadLink" />
|
|
|
|
<!-- QR code popup -->
|
|
<dialog id="dlgQR" class="backdrop:bg-black/75 rounded-md">
|
|
<form method="dialog">
|
|
<div class="text-slate-500 text-center">
|
|
<b>ID:</b> <code>did:peer:kl45kj41lk451kl3</code>
|
|
</div>
|
|
<img src="img/sample-qr-code.png" class="w-full mb-3" />
|
|
|
|
<button
|
|
value="cancel"
|
|
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
|
|
>
|
|
Copy to Clipboard
|
|
</button>
|
|
<button
|
|
value="cancel"
|
|
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md"
|
|
>
|
|
Close
|
|
</button>
|
|
</form>
|
|
</dialog>
|
|
|
|
<h3 class="text-sm uppercase font-semibold mb-3">Advanced</h3>
|
|
|
|
<label
|
|
for="toggleShowAmounts"
|
|
class="flex items-center cursor-pointer mb-6"
|
|
@click="handleChange"
|
|
>
|
|
<!-- toggle -->
|
|
<div class="relative">
|
|
<!-- input -->
|
|
<input
|
|
type="checkbox"
|
|
v-model="showContactGives"
|
|
name="showContactGives"
|
|
class="sr-only"
|
|
/>
|
|
<!-- line -->
|
|
<div class="block bg-slate-500 w-14 h-8 rounded-full"></div>
|
|
<!-- dot -->
|
|
<div
|
|
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
|
|
></div>
|
|
</div>
|
|
<!-- label -->
|
|
<div class="ml-2">Show amounts given with contacts</div>
|
|
</label>
|
|
|
|
<div class="flex py-2">
|
|
<button class="text-center text-md text-blue-500" @click="checkLimits()">
|
|
Check Limits
|
|
</button>
|
|
<div v-if="!!limits?.nextWeekBeginDateTime" class="px-9">
|
|
<span class="font-bold">Rate Limits</span>
|
|
<p>
|
|
You have done {{ limits.doneClaimsThisWeek }} claims out of
|
|
{{ limits.maxClaimsPerWeek }} for this week. Your claims counter
|
|
resets at {{ readableTime(limits.nextWeekBeginDateTime) }}
|
|
</p>
|
|
<p>
|
|
You have done {{ limits.doneRegistrationsThisMonth }} registrations
|
|
out of {{ limits.maxRegistrationsPerMonth }} for this month. Your
|
|
registrations counter resets at
|
|
{{ readableTime(limits.nextMonthBeginDateTime) }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex py-2">
|
|
Claim Server
|
|
<input
|
|
type="text"
|
|
class="block w-full rounded border border-slate-400 px-3 py-2"
|
|
v-model="apiServerInput"
|
|
/>
|
|
<button
|
|
v-if="apiServerInput != apiServer"
|
|
class="px-4 rounded bg-slate-200 border border-slate-400"
|
|
@click="onClickSaveApiServer()"
|
|
>
|
|
<fa icon="floppy-disk" class="fa-fw"></fa>
|
|
</button>
|
|
<button
|
|
class="px-4 rounded bg-slate-200 border border-slate-400"
|
|
@click="setApiServerInput(Constants.PROD_ENDORSER_API_SERVER)"
|
|
>
|
|
Use Prod
|
|
</button>
|
|
<button
|
|
class="px-4 rounded bg-slate-200 border border-slate-400"
|
|
@click="setApiServerInput(Constants.TEST_ENDORSER_API_SERVER)"
|
|
>
|
|
Use Test
|
|
</button>
|
|
<button
|
|
class="px-4 rounded bg-slate-200 border border-slate-400"
|
|
@click="setApiServerInput(Constants.LOCAL_ENDORSER_API_SERVER)"
|
|
>
|
|
Use Local
|
|
</button>
|
|
</div>
|
|
|
|
<div v-if="numAccounts > 0" class="flex py-2">
|
|
Switch Account
|
|
<span v-for="accountNum in numAccounts" :key="accountNum">
|
|
<button class="text-blue-500 px-2" @click="switchAccount(accountNum)">
|
|
#{{ accountNum }}
|
|
</button>
|
|
</span>
|
|
</div>
|
|
|
|
<div>
|
|
<button class="text-blue-500 px-2">
|
|
<router-link
|
|
:to="{ name: 'statistics' }"
|
|
class="block text-center py-3 px-1"
|
|
>
|
|
See Achievements & Statistics
|
|
</router-link>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- This same popup code is in many files. -->
|
|
<div v-bind:class="computedAlertClassNames()">
|
|
<button
|
|
class="close-button bg-slate-200 w-8 leading-loose rounded-full absolute top-2 right-2"
|
|
@click="onClickClose()"
|
|
>
|
|
<fa icon="xmark"></fa>
|
|
</button>
|
|
<h4 class="font-bold pr-5">{{ alertTitle }}</h4>
|
|
<p>{{ alertMessage }}</p>
|
|
</div>
|
|
</section>
|
|
</template>
|
|
|
|
<script lang="ts">
|
|
import "dexie-export-import";
|
|
import * as R from "ramda";
|
|
|
|
import { Component, Vue } from "vue-facing-decorator";
|
|
import { useClipboard } from "@vueuse/core";
|
|
|
|
import { AppString } from "@/constants/app";
|
|
import { db, accountsDB } from "@/db";
|
|
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
|
import {
|
|
accessToken,
|
|
deriveAddress,
|
|
generateSeed,
|
|
newIdentifier,
|
|
} from "@/libs/crypto";
|
|
import { AxiosError } from "axios/index";
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
const Buffer = require("buffer/").Buffer;
|
|
|
|
interface RateLimits {
|
|
doneClaimsThisWeek: string;
|
|
doneRegistrationsThisMonth: string;
|
|
maxClaimsPerWeek: string;
|
|
maxRegistrationsPerMonth: string;
|
|
nextMonthBeginDateTime: string;
|
|
nextWeekBeginDateTime: string;
|
|
}
|
|
|
|
@Component
|
|
export default class AccountViewView extends Vue {
|
|
Constants = AppString;
|
|
|
|
activeDid = "";
|
|
apiServer = "";
|
|
apiServerInput = "";
|
|
derivationPath = "";
|
|
firstName = "";
|
|
lastName = "";
|
|
numAccounts = 0;
|
|
publicHex = "";
|
|
publicBase64 = "";
|
|
limits: RateLimits | null = null;
|
|
showContactGives = false;
|
|
|
|
showDidCopy = false;
|
|
showDerCopy = false;
|
|
showB64Copy = false;
|
|
showPubCopy = false;
|
|
|
|
// call fn, copy text to the clipboard, then redo fn after 2 seconds
|
|
doCopyTwoSecRedo(text, fn) {
|
|
fn();
|
|
useClipboard()
|
|
.copy(text)
|
|
.then(() => setTimeout(fn, 2000));
|
|
}
|
|
|
|
handleChange() {
|
|
this.showContactGives = !this.showContactGives;
|
|
this.updateShowContactAmounts();
|
|
}
|
|
|
|
readableTime(timeStr: string) {
|
|
return timeStr.substring(0, timeStr.indexOf("T"));
|
|
}
|
|
|
|
// 'created' hook runs when the Vue instance is first created
|
|
async created() {
|
|
// Uncomment to register this user on the test server.
|
|
// To manage within the vue devtools browser extension https://devtools.vuejs.org/
|
|
// assign this to a class variable, eg. "registerThisUser = testServerRegisterUser",
|
|
// select a component in the extension, and enter in the console: $vm.ctx.registerThisUser()
|
|
//testServerRegisterUser();
|
|
try {
|
|
await db.open();
|
|
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
|
this.activeDid = settings?.activeDid || "";
|
|
this.apiServer = settings?.apiServer || "";
|
|
this.apiServerInput = settings?.apiServer || "";
|
|
this.firstName = settings?.firstName || "";
|
|
this.lastName = settings?.lastName || "";
|
|
this.showContactGives = !!settings?.showContactGivesInline;
|
|
|
|
await accountsDB.open();
|
|
this.numAccounts = await accountsDB.accounts.count();
|
|
if (this.numAccounts === 0) {
|
|
let address = ""; // 0x... ETH address, without "did:eth:"
|
|
let privateHex = "";
|
|
const mnemonic = generateSeed();
|
|
[address, privateHex, this.publicHex, this.derivationPath] =
|
|
deriveAddress(mnemonic);
|
|
|
|
const newId = newIdentifier(
|
|
address,
|
|
this.publicHex,
|
|
privateHex,
|
|
this.derivationPath
|
|
);
|
|
await accountsDB.accounts.add({
|
|
dateCreated: new Date().toISOString(),
|
|
derivationPath: this.derivationPath,
|
|
did: newId.did,
|
|
identity: JSON.stringify(newId),
|
|
mnemonic: mnemonic,
|
|
publicKeyHex: newId.keys[0].publicKeyHex,
|
|
});
|
|
this.activeDid = newId.did;
|
|
}
|
|
|
|
const accounts = await accountsDB.accounts.toArray();
|
|
const account = R.find((acc) => acc.did === this.activeDid, accounts);
|
|
const identity = JSON.parse(account?.identity || "undefined");
|
|
this.publicHex = identity.keys[0].publicKeyHex;
|
|
this.publicBase64 = Buffer.from(this.publicHex, "hex").toString("base64");
|
|
this.derivationPath = identity.keys[0].meta.derivationPath;
|
|
|
|
db.settings.update(MASTER_SETTINGS_KEY, {
|
|
activeDid: identity.did,
|
|
});
|
|
} catch (err) {
|
|
this.alertMessage =
|
|
"Clear your cache and start over (after data backup). See console log for more info.";
|
|
console.error("Telling user to clear cache because:", err);
|
|
this.alertTitle = "Error Creating Account";
|
|
this.isAlertVisible = true;
|
|
}
|
|
}
|
|
|
|
public async updateShowContactAmounts() {
|
|
try {
|
|
await db.open();
|
|
db.settings.update(MASTER_SETTINGS_KEY, {
|
|
showContactGivesInline: this.showContactGives,
|
|
});
|
|
} catch (err) {
|
|
this.alertMessage =
|
|
"Clear your cache and start over (after data backup). See console log for more info.";
|
|
console.error("Telling user to clear cache because:", err);
|
|
this.alertTitle = "Error Creating Account";
|
|
this.isAlertVisible = true;
|
|
}
|
|
}
|
|
|
|
public async exportDatabase() {
|
|
try {
|
|
const blob = await db.export({ prettyJson: true });
|
|
const url = URL.createObjectURL(blob);
|
|
|
|
const downloadAnchor = this.$refs.downloadLink as HTMLAnchorElement;
|
|
downloadAnchor.href = url;
|
|
downloadAnchor.download = db.name + "-backup.json";
|
|
downloadAnchor.click();
|
|
|
|
URL.revokeObjectURL(url);
|
|
|
|
this.alertTitle = "Download Started";
|
|
this.alertMessage = "See your downloads directory for the backup.";
|
|
this.isAlertVisible = true;
|
|
} catch (error) {
|
|
this.alertTitle = "Export Error";
|
|
this.alertMessage = "See console logs for more info.";
|
|
this.isAlertVisible = true;
|
|
console.error("Export Error:", error);
|
|
}
|
|
}
|
|
|
|
async checkLimits() {
|
|
const url = this.apiServer + "/api/report/rateLimits";
|
|
await accountsDB.open();
|
|
const accounts = await accountsDB.accounts.toArray();
|
|
const account = R.find((acc) => acc.did === this.activeDid, accounts);
|
|
const identity = JSON.parse(account?.identity || "undefined");
|
|
const token = await accessToken(identity);
|
|
const headers = {
|
|
"Content-Type": "application/json",
|
|
Authorization: "Bearer " + token,
|
|
};
|
|
|
|
try {
|
|
const resp = await this.axios.get(url, { headers });
|
|
// axios throws an exception on a 400
|
|
if (resp.status === 200) {
|
|
this.limits = resp.data;
|
|
}
|
|
} catch (error: unknown) {
|
|
const serverError = error as AxiosError;
|
|
|
|
this.alertTitle = "Error from Server";
|
|
console.error("Bad response retrieving limits: ", serverError);
|
|
// Anybody know how to access items inside "response.data" without this?
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const data: any = serverError.response?.data;
|
|
if (data.error.message) {
|
|
this.alertMessage = data.error.message;
|
|
} else {
|
|
this.alertMessage = "Bad server response. See logs for details.";
|
|
}
|
|
this.isAlertVisible = true;
|
|
}
|
|
}
|
|
|
|
async switchAccount(accountNum: number) {
|
|
await accountsDB.open();
|
|
const accounts = await accountsDB.accounts.toArray();
|
|
const account = accounts[accountNum - 1];
|
|
|
|
await db.open();
|
|
db.settings.update(MASTER_SETTINGS_KEY, {
|
|
activeDid: account.did,
|
|
});
|
|
|
|
this.activeDid = account.did;
|
|
this.derivationPath = account.derivationPath;
|
|
this.publicHex = account.publicKeyHex;
|
|
this.publicBase64 = Buffer.from(this.publicHex, "hex").toString("base64");
|
|
}
|
|
|
|
public showContactGivesClassNames() {
|
|
return {
|
|
"bg-slate-900": !this.showContactGives,
|
|
"bg-green-600": this.showContactGives,
|
|
};
|
|
}
|
|
|
|
async onClickSaveApiServer() {
|
|
await db.open();
|
|
db.settings.update(MASTER_SETTINGS_KEY, {
|
|
apiServer: this.apiServerInput,
|
|
});
|
|
this.apiServer = this.apiServerInput;
|
|
}
|
|
|
|
setApiServerInput(value) {
|
|
this.apiServerInput = value;
|
|
}
|
|
|
|
// This same popup code is in many files.
|
|
alertMessage = "";
|
|
alertTitle = "";
|
|
isAlertVisible = false;
|
|
public onClickClose() {
|
|
this.isAlertVisible = false;
|
|
this.alertTitle = "";
|
|
this.alertMessage = "";
|
|
}
|
|
public computedAlertClassNames() {
|
|
return {
|
|
hidden: !this.isAlertVisible,
|
|
"dismissable-alert": true,
|
|
"bg-slate-100": true,
|
|
"p-5": true,
|
|
rounded: true,
|
|
"drop-shadow-lg": true,
|
|
fixed: true,
|
|
"top-3": true,
|
|
"inset-x-3": true,
|
|
"transition-transform": true,
|
|
"ease-in": true,
|
|
"duration-300": true,
|
|
};
|
|
}
|
|
}
|
|
</script>
|
|
|