Browse Source

Merge pull request 'Add settings table.' (#12) from db-set-and-bak into master

Reviewed-on: https://gitea.anomalistdesign.com/trent_larson/kick-starter-for-time-pwa/pulls/12
kb/add-usage-guide
trentlarson 2 years ago
parent
commit
fb44c8aa48
  1. 1
      .tool-versions
  2. 12
      README.md
  3. 4
      project.yaml
  4. 32
      src/db/index.ts
  5. 12
      src/db/tables/accounts.ts
  6. 12
      src/db/tables/contacts.ts
  7. 41
      src/db/tables/index.ts
  8. 6
      src/libs/crypto/index.ts
  9. 10
      src/router/index.ts
  10. 22
      src/store/account.ts
  11. 15
      src/store/app.ts
  12. 194
      src/views/AccountViewView.vue
  13. 2
      src/views/ContactsView.vue
  14. 22
      src/views/ImportAccountView.vue
  15. 21
      src/views/NewEditAccountView.vue

1
.tool-versions

@ -1 +0,0 @@
nodejs 16.18.0

12
README.md

@ -36,9 +36,9 @@ New users require registration. This can be done with a claim payload like this
const vcClaim = { const vcClaim = {
"@context": "https://schema.org", "@context": "https://schema.org",
"@type": "RegisterAction", "@type": "RegisterAction",
agent: { did: identity0.did }, agent: { identifier: identity0.did },
object: SERVICE_ID, object: SERVICE_ID,
participant: { did: newIdentity.did }, participant: { identifier: newIdentity.did },
}; };
``` ```
@ -62,6 +62,14 @@ See [this page](openssl_signing_console.rst)
See [Configuration Reference](https://cli.vuejs.org/config/). See [Configuration Reference](https://cli.vuejs.org/config/).
## Dependencies
See https://tea.xyz
| Project | Version |
| ---------- | --------- |
| nodejs.org | ^16.0.0 |
| npmjs.com | ^8.0.0 |
## Other ## Other

4
project.yaml

@ -12,7 +12,9 @@
- replace user-affecting console.logs with error messages (eg. catches) - replace user-affecting console.logs with error messages (eg. catches)
- contacts v1: - contacts v1:
- parse input correctly (with CSV lib and not commas) - .5 make advanced "show/hide amounts" button into a nice UI toggle
- .2 show error to user when adding a duplicate contact
- parse input more robustly (with CSV lib and not commas)
- commit screen - commit screen

32
src/db/index.ts

@ -1,11 +1,19 @@
import BaseDexie, { Table } from "dexie"; import BaseDexie, { Table } from "dexie";
import { encrypted, Encryption } from "@pvermeer/dexie-encrypted-addon"; import { encrypted, Encryption } from "@pvermeer/dexie-encrypted-addon";
import { accountsSchema, Account } from "./tables/accounts"; import {
import { contactsSchema, Contact } from "./tables/contacts"; Account,
AccountsSchema,
Contact,
ContactsSchema,
MASTER_SETTINGS,
Settings,
SettingsSchema,
} from "./tables";
type AllTables = { type AllTables = {
accounts: Table<Account>; accounts: Table<Account>;
contacts: Table<Contact>; contacts: Table<Contact>;
settings: Table<Settings>;
}; };
/** /**
@ -18,7 +26,13 @@ type AllTables = {
*/ */
type DexieTables = AllTables; type DexieTables = AllTables;
export type Dexie<T extends unknown = DexieTables> = BaseDexie & T; export type Dexie<T extends unknown = DexieTables> = BaseDexie & T;
export const db = new BaseDexie("kickStarter") as Dexie; export const db = new BaseDexie("KickStart") as Dexie;
const AllSchemas = Object.assign(
{},
AccountsSchema,
ContactsSchema,
SettingsSchema
);
/** /**
* Needed to enable a special webpack setting to allow *await* below: * Needed to enable a special webpack setting to allow *await* below:
@ -32,7 +46,13 @@ const secret =
if (localStorage.getItem("secret") == null) { if (localStorage.getItem("secret") == null) {
localStorage.setItem("secret", secret); localStorage.setItem("secret", secret);
} }
console.log("DB encryption secretKey:", secret);
//console.log("IndexedDB Encryption Secret:", secret);
encrypted(db, { secretKey: secret }); encrypted(db, { secretKey: secret });
db.version(1).stores(accountsSchema); db.version(1).stores(AllSchemas);
db.version(2).stores(contactsSchema);
// initialize, a la https://dexie.org/docs/Tutorial/Design#the-populate-event
db.on("populate", function () {
// ensure there's an initial entry for settings
db.settings.add({ id: MASTER_SETTINGS });
});

12
src/db/tables/accounts.ts

@ -1,12 +0,0 @@
export type Account = {
id?: number;
publicKey: string;
mnemonic: string;
identity: string;
dateCreated: number;
};
// mark encrypted field by starting with a $ character
export const accountsSchema = {
accounts: "++id, publicKey, $mnemonic, $identity, dateCreated",
};

12
src/db/tables/contacts.ts

@ -1,12 +0,0 @@
export interface Contact {
did: string;
name?: string;
publicKeyBase64?: string;
seesMe?: boolean;
registered?: boolean;
}
// mark encrypted field by starting with a $ character
export const contactsSchema = {
contacts: "++did, name, publicKeyBase64, seesMe, registered",
};

41
src/db/tables/index.ts

@ -0,0 +1,41 @@
export type Account = {
id?: number; // auto-generated by Dexie
dateCreated: string;
derivationPath: string;
identity: string;
publicKeyHex: string;
mnemonic: string;
};
// mark encrypted field by starting with a $ character
// see https://github.com/PVermeer/dexie-addon-suite-monorepo/tree/master/packages/dexie-encrypted-addon
export const AccountsSchema = {
accounts:
"++id, dateCreated, derivationPath, $identity, $mnemonic, publicKeyHex",
};
export interface Contact {
did: string;
name?: string;
publicKeyBase64?: string;
seesMe?: boolean;
registered?: boolean;
}
export const ContactsSchema = {
contacts: "++did, name, publicKeyBase64, registered, seesMe",
};
// a singleton
export type Settings = {
id: number;
firstName?: string;
lastName?: string;
showContactGivesInline?: boolean;
};
export const SettingsSchema = {
settings: "id",
};
export const MASTER_SETTINGS = 1;

6
src/libs/crypto/index.ts

@ -65,7 +65,7 @@ export const deriveAddress = (
* *
* @return {*} {string} * @return {*} {string}
*/ */
export const createIdentifier = (): string => { export const generateSeed = (): string => {
const entropy: Uint8Array = getRandomBytesSync(32); const entropy: Uint8Array = getRandomBytesSync(32);
const mnemonic = entropyToMnemonic(entropy, wordlist); const mnemonic = entropyToMnemonic(entropy, wordlist);
@ -87,9 +87,9 @@ export const accessToken = async (identifier: IIdentifier) => {
const nowEpoch = Math.floor(Date.now() / 1000); const nowEpoch = Math.floor(Date.now() / 1000);
const endEpoch = nowEpoch + 60; // add one minute const endEpoch = nowEpoch + 60; // add one minute
const uportTokenPayload = { exp: endEpoch, iat: nowEpoch, iss: did }; const tokenPayload = { exp: endEpoch, iat: nowEpoch, iss: did };
const alg = undefined; // defaults to 'ES256K', more standardized but harder to verify vs ES256K-R const alg = undefined; // defaults to 'ES256K', more standardized but harder to verify vs ES256K-R
const jwt: string = await didJwt.createJWT(uportTokenPayload, { const jwt: string = await didJwt.createJWT(tokenPayload, {
alg, alg,
issuer: did, issuer: did,
signer, signer,

10
src/router/index.ts

@ -1,5 +1,5 @@
import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router"; import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router";
import { useAppStore } from "../store/app"; import { db } from "@/db";
const routes: Array<RouteRecordRaw> = [ const routes: Array<RouteRecordRaw> = [
{ {
@ -7,10 +7,10 @@ const routes: Array<RouteRecordRaw> = [
name: "home", name: "home",
component: () => component: () =>
import(/* webpackChunkName: "start" */ "../views/DiscoverView.vue"), import(/* webpackChunkName: "start" */ "../views/DiscoverView.vue"),
beforeEnter: (to, from, next) => { beforeEnter: async (to, from, next) => {
const appStore = useAppStore(); await db.open();
const isAuthenticated = appStore.condition === "registered"; const num_accounts = await db.accounts.count();
if (isAuthenticated) { if (num_accounts > 0) {
next(); next();
} else { } else {
next({ name: "start" }); next({ name: "start" });

22
src/store/account.ts

@ -1,22 +0,0 @@
// @ts-check
import { defineStore } from "pinia";
export const useAccountStore = defineStore({
id: "account",
state: () => ({
account: JSON.parse(
typeof localStorage["account"] == "undefined"
? null
: localStorage["account"]
),
}),
getters: {
firstName: (state) => state.account.firstName,
lastName: (state) => state.account.lastName,
},
actions: {
reset() {
localStorage.removeItem("account");
},
},
});

15
src/store/app.ts

@ -4,30 +4,15 @@ import { defineStore } from "pinia";
export const useAppStore = defineStore({ export const useAppStore = defineStore({
id: "app", id: "app",
state: () => ({ state: () => ({
_condition:
typeof localStorage["condition"] == "undefined"
? "uninitialized"
: localStorage["condition"],
_lastView:
typeof localStorage["lastView"] == "undefined"
? "/start"
: localStorage["lastView"],
_projectId: _projectId:
typeof localStorage.getItem("projectId") === "undefined" typeof localStorage.getItem("projectId") === "undefined"
? "" ? ""
: localStorage.getItem("projectId"), : localStorage.getItem("projectId"),
}), }),
getters: { getters: {
condition: (state) => state._condition,
projectId: (state): string => state._projectId as string, projectId: (state): string => state._projectId as string,
}, },
actions: { actions: {
reset() {
localStorage.removeItem("condition");
},
setCondition(newCondition: string) {
localStorage.setItem("condition", newCondition);
},
async setProjectId(newProjectId: string) { async setProjectId(newProjectId: string) {
localStorage.setItem("projectId", newProjectId); localStorage.setItem("projectId", newProjectId);
}, },

194
src/views/AccountViewView.vue

@ -111,8 +111,8 @@
<div class="text-slate-500 text-sm font-bold">Derivation Path</div> <div class="text-slate-500 text-sm font-bold">Derivation Path</div>
<div class="text-sm text-slate-500 mb-1"> <div class="text-sm text-slate-500 mb-1">
<span <span
><code>{{ UPORT_ROOT_DERIVATION_PATH }}</code> ><code>{{ derivationPath }}</code>
<button @click="copy(UPORT_ROOT_DERIVATION_PATH)"> <button @click="copy(derivationPath)">
<fa icon="copy" class="text-slate-400 fa-fw ml-1"></fa> <fa icon="copy" class="text-slate-400 fa-fw ml-1"></fa>
</button> </button>
</span> </span>
@ -168,86 +168,158 @@
</button> </button>
</form> </form>
</dialog> </dialog>
<h3 class="text-sm uppercase font-semibold mb-3">Advanced</h3>
<div class="flex">
<button
href=""
class="text-center text-md text-white px-1.5 py-2 rounded-md mb-6"
v-bind:class="showContactGivesClassNames()"
@click="toggleShowContactAmounts"
>
{{ showContactGives ? "Showing" : "Hiding" }}
amounts given with contacts (Click to
{{ showContactGives ? "hide" : "show" }})
</button>
</div>
<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> </section>
</template> </template>
<script lang="ts"> <script lang="ts">
import { Options, Vue } from "vue-class-component"; import { Options, Vue } from "vue-class-component";
import { useClipboard } from "@vueuse/core"; import { useClipboard } from "@vueuse/core";
import { createIdentifier, deriveAddress, newIdentifier } from "../libs/crypto"; import { db } from "@/db";
import { db } from "../db"; import { MASTER_SETTINGS } from "@/db/tables";
import { useAppStore } from "@/store/app"; import { deriveAddress, generateSeed, newIdentifier } from "@/libs/crypto";
import { ref } from "vue";
//import { testServerRegisterUser } from "../test"; //import { testServerRegisterUser } from "../test";
@Options({ @Options({
components: {}, components: {},
}) })
export default class AccountViewView extends Vue { export default class AccountViewView extends Vue {
firstName = // This registers current user in vue plugin with: $vm.ctx.testRegisterUser()
localStorage.getItem("firstName") === null //testRegisterUser = testServerRegisterUser;
? "--"
: localStorage.getItem("firstName");
lastName =
localStorage.getItem("lastName") === null
? "--"
: localStorage.getItem("lastName");
mnemonic = "";
address = ""; address = "";
privateHex = ""; firstName = "";
lastName = "";
mnemonic = "";
publicHex = ""; publicHex = "";
UPORT_ROOT_DERIVATION_PATH = ""; derivationPath = "";
source = ref("Hello"); showContactGives = false;
copy = useClipboard().copy;
// This registers current user in vue plugin with: $vm.ctx.testRegisterUser() copy = useClipboard().copy;
//testRegisterUser = testServerRegisterUser;
// 'created' hook runs when the Vue instance is first created // 'created' hook runs when the Vue instance is first created
async created() { async created() {
const appCondition = useAppStore().condition; await db.open();
if (appCondition == "uninitialized") { try {
this.mnemonic = createIdentifier(); const settings = await db.settings.get(MASTER_SETTINGS);
[ if (settings) {
this.address, this.firstName = settings.firstName || "";
this.privateHex, this.lastName = settings.lastName || "";
this.publicHex, this.showContactGives = !!settings.showContactGivesInline;
this.UPORT_ROOT_DERIVATION_PATH,
] = deriveAddress(this.mnemonic);
const newId = newIdentifier(
this.address,
this.publicHex,
this.privateHex,
this.UPORT_ROOT_DERIVATION_PATH
);
try {
await db.open();
const num_accounts = await db.accounts.count();
if (num_accounts === 0) {
await db.accounts.add({
publicKey: newId.keys[0].publicKeyHex,
mnemonic: this.mnemonic,
identity: JSON.stringify(newId),
dateCreated: new Date().getTime(),
});
}
useAppStore().setCondition("registered");
} catch (err) {
console.log(err);
} }
const numAccounts = await db.accounts.count();
if (numAccounts === 0) {
let privateHex = "";
this.mnemonic = generateSeed();
[this.address, privateHex, this.publicHex, this.derivationPath] =
deriveAddress(this.mnemonic);
const newId = newIdentifier(
this.address,
this.publicHex,
privateHex,
this.derivationPath
);
await db.accounts.add({
dateCreated: new Date().toISOString(),
derivationPath: this.derivationPath,
identity: JSON.stringify(newId),
mnemonic: this.mnemonic,
publicKeyHex: newId.keys[0].publicKeyHex,
});
}
} catch (err) {
this.alertMessage =
"Clear your cache and start over (after data backup). See console log for more info.";
console.log("Telling user to clear cache because:", err);
this.alertTitle = "Error Creating Account";
this.isAlertVisible = true;
} }
await db.open();
const num_accounts = await db.accounts.count(); const accounts = await db.accounts.toArray();
if (num_accounts === 0) { const identity = JSON.parse(accounts[0].identity);
console.log("Problem! Should have a profile!"); this.address = identity.did;
} else { this.publicHex = identity.keys[0].publicKeyHex;
const accounts = await db.accounts.toArray(); this.derivationPath = identity.keys[0].meta.derivationPath;
const identity = JSON.parse(accounts[0].identity); }
this.address = identity.did;
this.publicHex = identity.keys[0].publicKeyHex; public async toggleShowContactAmounts() {
this.UPORT_ROOT_DERIVATION_PATH = identity.keys[0].meta.derivationPath; this.showContactGives = !this.showContactGives;
try {
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS);
if (settings) {
db.settings.update(MASTER_SETTINGS, {
showContactGivesInline: this.showContactGives,
});
}
} catch (err) {
this.alertMessage =
"Clear your cache and start over (after data backup). See console log for more info.";
console.log("Telling user to clear cache because:", err);
this.alertTitle = "Error Creating Account";
this.isAlertVisible = true;
} }
} }
public showContactGivesClassNames() {
return {
"bg-slate-900": !this.showContactGives,
"bg-green-600": this.showContactGives,
};
}
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,
absolute: true,
"top-3": true,
"inset-x-3": true,
"transition-transform": true,
"ease-in": true,
"duration-300": true,
};
}
} }
</script> </script>

2
src/views/ContactsView.vue

@ -141,7 +141,7 @@ import { AppString } from "@/constants/app";
import { accessToken, SimpleSigner } from "@/libs/crypto"; import { accessToken, SimpleSigner } from "@/libs/crypto";
import { IIdentifier } from "@veramo/core"; import { IIdentifier } from "@veramo/core";
import { db } from "../db"; import { db } from "../db";
import { Contact } from "../db/tables/contacts"; import { Contact } from "../db/tables";
export interface GiveVerifiableCredential { export interface GiveVerifiableCredential {
"@context": string; "@context": string;

22
src/views/ImportAccountView.vue

@ -46,7 +46,6 @@
import { Options, Vue } from "vue-class-component"; import { Options, Vue } from "vue-class-component";
import { deriveAddress, newIdentifier } from "../libs/crypto"; import { deriveAddress, newIdentifier } from "../libs/crypto";
import { db } from "@/db"; import { db } from "@/db";
import { useAppStore } from "@/store/app";
@Options({ @Options({
components: {}, components: {},
@ -56,7 +55,7 @@ export default class ImportAccountView extends Vue {
address = ""; address = "";
privateHex = ""; privateHex = "";
publicHex = ""; publicHex = "";
UPORT_ROOT_DERIVATION_PATH = ""; derivationPath = "";
public onCancelClick() { public onCancelClick() {
this.$router.back(); this.$router.back();
@ -65,33 +64,28 @@ export default class ImportAccountView extends Vue {
public async from_mnemonic() { public async from_mnemonic() {
const mne: string = this.mnemonic.trim().toLowerCase(); const mne: string = this.mnemonic.trim().toLowerCase();
if (this.mnemonic.trim().length > 0) { if (this.mnemonic.trim().length > 0) {
[ [this.address, this.privateHex, this.publicHex, this.derivationPath] =
this.address, deriveAddress(mne);
this.privateHex,
this.publicHex,
this.UPORT_ROOT_DERIVATION_PATH,
] = deriveAddress(mne);
const newId = newIdentifier( const newId = newIdentifier(
this.address, this.address,
this.publicHex, this.publicHex,
this.privateHex, this.privateHex,
this.UPORT_ROOT_DERIVATION_PATH this.derivationPath
); );
try { try {
await db.open(); await db.open();
const num_accounts = await db.accounts.count(); const num_accounts = await db.accounts.count();
if (num_accounts === 0) { if (num_accounts === 0) {
console.log("...");
await db.accounts.add({ await db.accounts.add({
publicKey: newId.keys[0].publicKeyHex, dateCreated: new Date().toISOString(),
mnemonic: mne, derivationPath: this.derivationPath,
identity: JSON.stringify(newId), identity: JSON.stringify(newId),
dateCreated: new Date().getTime(), mnemonic: mne,
publicKeyHex: newId.keys[0].publicKeyHex,
}); });
} }
useAppStore().setCondition("registered");
this.$router.push({ name: "account" }); this.$router.push({ name: "account" });
} catch (err) { } catch (err) {
console.log("Error!"); console.log("Error!");

21
src/views/NewEditAccountView.vue

@ -50,6 +50,8 @@
<script lang="ts"> <script lang="ts">
import { Options, Vue } from "vue-class-component"; import { Options, Vue } from "vue-class-component";
import { db } from "@/db";
import { MASTER_SETTINGS } from "@/db/tables";
@Options({ @Options({
components: {}, components: {},
@ -64,13 +66,24 @@ export default class NewEditAccountView extends Vue {
? "--" ? "--"
: localStorage.getItem("lastName"); : localStorage.getItem("lastName");
// 'created' hook runs when the Vue instance is first created
async created() {
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS);
if (settings) {
this.firstName = settings.firstName || "";
this.lastName = settings.lastName || "";
}
}
onClickSaveChanges() { onClickSaveChanges() {
db.settings.update(MASTER_SETTINGS, {
firstName: this.firstName,
lastName: this.lastName,
});
localStorage.setItem("firstName", this.firstName as string); localStorage.setItem("firstName", this.firstName as string);
localStorage.setItem("lastName", this.lastName as string); localStorage.setItem("lastName", this.lastName as string);
const route = { this.$router.push({ name: "account" });
name: "account",
};
this.$router.push(route);
} }
onClickCancel() { onClickCancel() {

Loading…
Cancel
Save