Compare commits

...

26 Commits

Author SHA1 Message Date
Matthew Aaron Raymer
1c0881fe14 Subject: workflow for saving of project
* Changed from passing parameters since this isn't supported anymore by Vue
* Added new AppStore values for a projectId which are accessible in the ProjectView View
2023-01-10 16:22:07 +08:00
2a7c858662 refactor: use our own SimpleSigner since library version is deprecated 2023-01-09 19:08:41 -07:00
Matthew Aaron Raymer
41d8df2238 Added Project New Button 2023-01-09 16:05:45 +08:00
Matthew Aaron Raymer
71546ea605 Added new projects view 2023-01-09 15:57:58 +08:00
Matthew Aaron Raymer
487997b87c Adding a less complex router 2023-01-09 15:10:05 +08:00
Matthew Aaron Raymer
ba85663048 Linted 2023-01-07 13:19:04 +08:00
9d566fa977 fix: use SimpleSigner directly from did-jwt 2023-01-06 19:25:53 -07:00
Matthew Aaron Raymer
3440f28121 Added some ideas for doing actions from the console ... Work in Progress 2023-01-06 17:56:37 +08:00
Matthew Aaron Raymer
150b35c4c7 Update with our own SimpleSigner 2023-01-06 17:30:41 +08:00
Matthew Aaron Raymer
39c931cde9 A touch of documenting process 2023-01-04 17:15:11 +08:00
Matthew Aaron Raymer
f858a4d29a Linted 2023-01-04 17:09:11 +08:00
Matthew Aaron Raymer
f021fcdb1c New Project 2023-01-04 17:05:08 +08:00
Matthew Aaron Raymer
6325bcbe35 Work in progress 2023-01-03 17:28:23 +08:00
Matthew Aaron Raymer
0ee35e4946 Added variation of accessToken method carried over from endorser-mobile 2023-01-03 12:35:41 +08:00
Matthew Aaron Raymer
f5a2d71ed3 Added some elementary Vue constructs 2023-01-02 17:55:43 +08:00
c43fdcbb7f Merge pull request 'chore: add setup file for asdf-vm.com' (#3) from setup-asdf into master
Reviewed-on: trent_larson/kick-starter-for-time-pwa#3
2023-01-02 01:53:16 -05:00
3034d66a5d docs: insert coding project task list into this repo 2023-01-01 12:48:42 -07:00
ba14fd4242 chore: add setup file for asdf-vm.com 2023-01-01 12:43:34 -07:00
236c1c2836 Merge pull request 'remove code for lowercase checks (that were for old uPort)' (#1) from remove-unused-lowercases into master
Reviewed-on: trent_larson/kick-starter-for-time-pwa#1
2022-12-31 21:03:38 -05:00
d2cea34242 fix: remove code for lowercase checks (that were for old uPort) 2022-12-29 16:13:51 -07:00
Matthew Aaron Raymer
3687e5e282 Subject line: Added v-model for input box on seed input.
Also completed recovery from seed.  Should be ready to move to Make commitment
2022-12-22 15:24:17 +08:00
Matthew Aaron Raymer
65381e103c Flesh out a bit more of the import and also refactor the router 2022-12-21 18:02:19 +08:00
Matthew Aaron Raymer
07f763e167 Fixing import function 2022-12-21 16:08:40 +08:00
Matthew Aaron Raymer
c9919987ca Added clipboard copy actions.
We need to add designs for feedback on the copy action
2022-12-19 17:51:14 +08:00
Matthew Aaron Raymer
9f94aad88a Subject line: Adding routing for post-registration.
Had to add a bogus home page (left over boilerplate home) to fill the role of an initial load state.
The app will look at the app Pinia state and if it is 'registered' and then send to the discovery page.
2022-12-19 15:58:17 +08:00
Matthew Aaron Raymer
b4557c3596 Adding markers to keep track of registration state 2022-12-19 14:19:33 +08:00
17 changed files with 598 additions and 189 deletions

1
.tool-versions Normal file
View File

@@ -0,0 +1 @@
nodejs 16.18.0

View File

@@ -23,7 +23,13 @@ npm run lint
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).
## Other
```
// reference material from https://github.com/trentlarson/endorser-mobile/blob/8dc8e0353e0cc80ffa7ed89ded15c8b0da92726b/src/utility/idUtility.ts#L83
// Import an existing ID
export const importAndStoreIdentifier = async (mnemonic: string, mnemonicPassword: string, toLowercase: boolean, previousIdentifiers: Array<IIdentifier>) => {
@@ -122,4 +128,4 @@ export const createAndStoreIdentifier = async (mnemonicPassword) => {
return importAndStoreIdentifier(mnemonic, mnemonicPassword, false, [])
}
```
```

View File

@@ -0,0 +1,45 @@
You can create a JWT using a library or by encoding the header and payload base64Url and signing it with a secret using a ES256K algorithm. Here is an example of how you can create a JWT using the jq and openssl command line utilities:
Here is an example of how you can use openssl to sign a JWT with the ES256K algorithm:
Generate an ECDSA key pair using the secp256k1 curve:
openssl ecparam -name secp256k1 -genkey -noout -out private.pem
openssl ec -in private.pem -pubout -out public.pem
First, create a header object as a JSON object containing the alg (algorithm) and typ (type) fields. For example:
header='{"alg":"ES256K", "issuer": "", "typ":"JWT"}'
Next, create a payload object as a JSON object containing the claims you want to include in the JWT. For example schema.org :
payload='{"@context": "http://schema.org", "@type": "PlanAction", "identifier": "did:ethr:0xb86913f83A867b5Ef04902419614A6FF67466c12", "name": "Test", "description": "Me"}'
Encode the header and payload objects as base64Url strings. You can use the jq command line utility to do this:
header_b64=$(echo -n "$header" | jq -c -M . | tr -d '\n')
payload_b64=$(echo -n "$payload" | jq -c -M . | tr -d '\n')
Concatenate the encoded header, payload, and a secret to create the signing input:
signing_input="$header_b64.$payload_b64"
Create the signature by signing the signing input with a ES256K algorithm and your secret. You can use the openssl command line utility to do this:
signature=$(echo -n "$signing_input" | openssl dgst -sha256 -sign private.pem)
Finally, encode the signature as a base64Url string and concatenate it with the signing input to create the JWT:
signature_b64=$(echo -n "$signature" | base64 | tr -d '=' | tr '+' '-' | tr '/' '_')
jwt="$signing_input.$signature_b64"
This JWT can then be passed in the Authorization header of a HTTP request as a bearer token, for example:
Authorization: Bearer $jwt
To verify the JWT, you can use the openssl utility with the public key:
openssl dgst -sha256 -verify public.pem -signature <(echo -n "$signature") "$signing_input"
This will verify the signature and output Verified OK if the signature is valid. If the signature is not valid, it will output an error.

57
package-lock.json generated
View File

@@ -22,9 +22,11 @@
"@veramo/key-manager": "^4.1.1",
"@vueuse/core": "^9.6.0",
"@zxing/text-encoding": "^0.9.0",
"axios": "^1.2.2",
"class-transformer": "^0.5.1",
"core-js": "^3.26.1",
"dexie": "^3.2.2",
"did-jwt": "^6.9.0",
"ethereum-cryptography": "^1.1.2",
"ethereumjs-util": "^7.1.5",
"ethr-did-resolver": "^8.0.0",
@@ -40,6 +42,7 @@
"reflect-metadata": "^0.1.13",
"register-service-worker": "^1.7.2",
"vue": "^3.2.45",
"vue-axios": "^3.5.2",
"vue-class-component": "^8.0.0-0",
"vue-property-decorator": "^9.1.2",
"vue-router": "^4.1.6",
@@ -9685,9 +9688,7 @@
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"optional": true,
"peer": true
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"node_modules/at-least-node": {
"version": "1.0.0",
@@ -9744,6 +9745,29 @@
"postcss": "^8.1.0"
}
},
"node_modules/axios": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.2.2.tgz",
"integrity": "sha512-bz/J4gS2S3I7mpN/YZfGFTqhXTYzRho8Ay38w2otuuDR322KzFIWm/4W2K6gIwvWaws5n+mnb7D1lN9uD+QH6Q==",
"dependencies": {
"follow-redirects": "^1.15.0",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/axios/node_modules/form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/b64-lite": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/b64-lite/-/b64-lite-1.4.0.tgz",
@@ -11025,8 +11049,6 @@
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"optional": true,
"peer": true,
"dependencies": {
"delayed-stream": "~1.0.0"
},
@@ -12103,8 +12125,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"optional": true,
"peer": true,
"engines": {
"node": ">=0.4.0"
}
@@ -12167,9 +12187,9 @@
}
},
"node_modules/did-jwt": {
"version": "6.10.1",
"resolved": "https://registry.npmjs.org/did-jwt/-/did-jwt-6.10.1.tgz",
"integrity": "sha512-YJOvkuPKKX364ooAFNxZPcz/KBLRwLhRABQVQlVEqOjygsCkplNFB3UL97UqZ7Y3cAG6Jh5jKoAC4xFSm+h0qw==",
"version": "6.9.0",
"resolved": "https://registry.npmjs.org/did-jwt/-/did-jwt-6.9.0.tgz",
"integrity": "sha512-kZ8pakovM2VkG0pia6x0SA9/1rl9dOUti4i2FL3xg7arJDWW7dACJxX+6gQK7iR/DvXrfFo8F784ejHVbw9ryA==",
"dependencies": {
"@stablelib/ed25519": "^1.0.2",
"@stablelib/random": "^1.0.1",
@@ -14582,7 +14602,6 @@
"version": "1.15.2",
"resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.2.tgz",
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
"dev": true,
"funding": [
{
"type": "individual",
@@ -19049,7 +19068,6 @@
"version": "1.52.0",
"resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"devOptional": true,
"engines": {
"node": ">= 0.6"
}
@@ -19058,7 +19076,6 @@
"version": "2.1.35",
"resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"devOptional": true,
"dependencies": {
"mime-db": "1.52.0"
},
@@ -21430,6 +21447,11 @@
"node": ">= 0.10"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
},
"node_modules/pseudomap": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/pseudomap/-/pseudomap-1.0.2.tgz",
@@ -25254,6 +25276,15 @@
"@vue/shared": "3.2.45"
}
},
"node_modules/vue-axios": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/vue-axios/-/vue-axios-3.5.2.tgz",
"integrity": "sha512-GP+dct7UlAWkl1qoP3ppw0z6jcSua5/IrMpjB5O8bh089iIiJ+hdxPYH2NPEpajlYgkW5EVMP95ttXWdas1O0g==",
"peerDependencies": {
"axios": "*",
"vue": "^3.0.0 || ^2.0.0"
}
},
"node_modules/vue-class-component": {
"version": "8.0.0-rc.1",
"resolved": "https://registry.npmmirror.com/vue-class-component/-/vue-class-component-8.0.0-rc.1.tgz",

View File

@@ -22,9 +22,11 @@
"@veramo/key-manager": "^4.1.1",
"@vueuse/core": "^9.6.0",
"@zxing/text-encoding": "^0.9.0",
"axios": "^1.2.2",
"class-transformer": "^0.5.1",
"core-js": "^3.26.1",
"dexie": "^3.2.2",
"did-jwt": "^6.9.0",
"ethereum-cryptography": "^1.1.2",
"ethereumjs-util": "^7.1.5",
"ethr-did-resolver": "^8.0.0",
@@ -40,6 +42,7 @@
"reflect-metadata": "^0.1.13",
"register-service-worker": "^1.7.2",
"vue": "^3.2.45",
"vue-axios": "^3.5.2",
"vue-class-component": "^8.0.0-0",
"vue-property-decorator": "^9.1.2",
"vue-router": "^4.1.6",

6
project.yaml Normal file
View File

@@ -0,0 +1,6 @@
- top screens from img/screens.pdf :
- discover
- view
- new
- commit
blocks: ref:https://raw.githubusercontent.com/trentlarson/lives-of-gifts/master/project.yaml#kickstarter%20for%20time

View File

@@ -4,4 +4,6 @@
export enum AppString {
APP_NAME = "Kickstart for time",
VERSION = "0.1",
DEFAULT_ENDORSER_API_SERVER = "https://test.endorser.ch:8000",
DEFAULT_ENDORSER_VIEW_SERVER = "https://test.endorser.ch",
}

View File

@@ -4,6 +4,8 @@ import { getRandomBytesSync } from "ethereum-cryptography/random";
import { entropyToMnemonic } from "ethereum-cryptography/bip39";
import { wordlist } from "ethereum-cryptography/bip39/wordlists/english";
import { HDNode } from "@ethersproject/hdnode";
import * as didJwt from "did-jwt";
import * as u8a from "uint8arrays";
/**
*
@@ -37,6 +39,12 @@ export const newIdentifier = (
};
};
/**
*
*
* @param {string} mnemonic
* @return {*} {[string, string, string, string]}
*/
export const deriveAddress = (
mnemonic: string
): [string, string, string, string] => {
@@ -63,3 +71,80 @@ export const createIdentifier = (): string => {
return mnemonic;
};
/**
* Retreive an access token
*
* @param {IIdentifier} identifier
* @return {*}
*/
export const accessToken = async (identifier: IIdentifier) => {
const did: string = identifier.did;
const privateKeyHex: string = identifier.keys[0].privateKeyHex as string;
const signer = SimpleSigner(privateKeyHex);
const nowEpoch = Math.floor(Date.now() / 1000);
const endEpoch = nowEpoch + 60; // add one minute
const uportTokenPayload = { exp: endEpoch, iat: nowEpoch, iss: did };
const alg = undefined; // defaults to 'ES256K', more standardized but harder to verify vs ES256K-R
const jwt: string = await didJwt.createJWT(uportTokenPayload, {
alg,
issuer: did,
signer,
});
return jwt;
};
export const sign = async (privateKeyHex: string) => {
const signer = SimpleSigner(privateKeyHex);
return signer;
};
/**
* Copied out of did-jwt since it's deprecated in that library.
*
* The SimpleSigner returns a configured function for signing data.
*
* @example
* const signer = SimpleSigner(process.env.PRIVATE_KEY)
* signer(data, (err, signature) => {
* ...
* })
*
* @param {String} hexPrivateKey a hex encoded private key
* @return {Function} a configured signer function
*/
export function SimpleSigner(hexPrivateKey: string): didJwt.Signer {
const signer = didJwt.ES256KSigner(didJwt.hexToBytes(hexPrivateKey), true);
return async (data) => {
const signature = (await signer(data)) as string;
return fromJose(signature);
};
}
// from did-jwt/util; see SimpleSigner above
export function fromJose(signature: string): {
r: string;
s: string;
recoveryParam?: number;
} {
const signatureBytes: Uint8Array = didJwt.base64ToBytes(signature);
if (signatureBytes.length < 64 || signatureBytes.length > 65) {
throw new TypeError(
`Wrong size for signature. Expected 64 or 65 bytes, but got ${signatureBytes.length}`
);
}
const r = bytesToHex(signatureBytes.slice(0, 32));
const s = bytesToHex(signatureBytes.slice(32, 64));
const recoveryParam =
signatureBytes.length === 65 ? signatureBytes[64] : undefined;
return { r, s, recoveryParam };
}
// from did-jwt/util; see SimpleSigner above
export function bytesToHex(b: Uint8Array): string {
return u8a.toString(b, "base16");
}

View File

@@ -3,6 +3,9 @@ import { createApp } from "vue";
import App from "./App.vue";
import "./registerServiceWorker";
import router from "./router";
import axios from "axios";
import VueAxios from "vue-axios";
import "./assets/styles/tailwind.css";
import { library } from "@fortawesome/fontawesome-svg-core";
@@ -18,6 +21,7 @@ import {
faQrcode,
faUser,
faPen,
faPlus,
faTrashCan,
faCalendar,
faEllipsisVertical,
@@ -37,6 +41,7 @@ library.add(
faQrcode,
faUser,
faPen,
faPlus,
faTrashCan,
faCalendar,
faEllipsisVertical,
@@ -49,5 +54,6 @@ import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
createApp(App)
.component("fa", FontAwesomeIcon)
.use(createPinia())
.use(VueAxios, axios)
.use(router)
.mount("#app");

View File

@@ -1,19 +1,25 @@
import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router";
import { useAppStore } from "../store/app";
import HomeView from "../views/HomeView.vue";
const routes: Array<RouteRecordRaw> = [
{
path: "/",
name: "home",
component: HomeView,
component: () =>
import(/* webpackChunkName: "start" */ "../views/DiscoverView.vue"),
beforeEnter: (to, from, next) => {
const appStore = useAppStore();
const isAuthenticated = appStore.condition === "registered";
if (isAuthenticated) {
next();
} else {
next({ name: "start" });
}
},
},
{
path: "/about",
name: "about",
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () =>
import(/* webpackChunkName: "about" */ "../views/AboutView.vue"),
},
@@ -75,6 +81,12 @@ const routes: Array<RouteRecordRaw> = [
/* webpackChunkName: "new-edit-commitment" */ "../views/NewEditCommitmentView.vue"
),
},
{
path: "/project",
name: "project",
component: () =>
import(/* webpackChunkName: "project" */ "../views/ProjectViewView.vue"),
},
{
path: "/new-edit-project",
name: "new-edit-project",
@@ -83,12 +95,6 @@ const routes: Array<RouteRecordRaw> = [
/* webpackChunkName: "new-edit-project" */ "../views/NewEditProjectView.vue"
),
},
{
path: "/project",
name: "project",
component: () =>
import(/* webpackChunkName: "project" */ "../views/ProjectViewView.vue"),
},
{
path: "/projects",
name: "projects",
@@ -105,37 +111,10 @@ const routes: Array<RouteRecordRaw> = [
},
];
/** @type {*} */
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes,
});
router.beforeEach(async (to) => {
const publicPages = ["/start", "/account", "/import-account"];
const isPublic = publicPages.includes(to.path);
const appStore = useAppStore();
let return_path = "/start";
if (isPublic) {
switch (appStore.condition) {
case "uninitialized":
return_path = "";
break;
case "registering":
return_path = to.path;
break;
}
} else {
switch (appStore.condition) {
case "registered":
return_path = to.path;
}
}
if (return_path == "") {
return;
} else {
return return_path;
}
});
export default router;

View File

@@ -12,13 +12,24 @@ export const useAppStore = defineStore({
typeof localStorage["lastView"] == "undefined"
? "/start"
: localStorage["lastView"],
_projectId:
typeof localStorage.getItem("projectId") === "undefined"
? ""
: localStorage.getItem("projectId"),
}),
getters: {
condition: (state) => state._condition,
projectId: (state): string => state._projectId as string,
},
actions: {
reset() {
localStorage.removeItem("condition");
},
setCondition(newCondition: string) {
localStorage.setItem("condition", newCondition);
},
setProjectId(newProjectId: string) {
localStorage.setItem("projectId", newProjectId);
},
},
});

View File

@@ -80,7 +80,9 @@
>
<span
><code>{{ address }}</code>
<fa icon="copy" class="text-slate-400 fa-fw ml-1"></fa>
<button @click="copy(address)">
<fa icon="copy" class="text-slate-400 fa-fw ml-1"></fa>
</button>
</span>
<span>
<button
@@ -100,7 +102,9 @@
<div class="text-sm text-slate-500 mb-1">
<span
><code>{{ publicHex }}</code>
<fa icon="copy" class="text-slate-400 fa-fw ml-1"></fa>
<button @click="copy(publicHex)">
<fa icon="copy" class="text-slate-400 fa-fw ml-1"></fa>
</button>
</span>
</div>
@@ -108,7 +112,9 @@
<div class="text-sm text-slate-500 mb-1">
<span
><code>{{ UPORT_ROOT_DERIVATION_PATH }}</code>
<fa icon="copy" class="text-slate-400 fa-fw ml-1"></fa>
<button @click="copy(UPORT_ROOT_DERIVATION_PATH)">
<fa icon="copy" class="text-slate-400 fa-fw ml-1"></fa>
</button>
</span>
</div>
</div>
@@ -167,10 +173,11 @@
<script lang="ts">
import { Options, Vue } from "vue-class-component";
import { useClipboard } from "@vueuse/core";
import { createIdentifier, deriveAddress, newIdentifier } from "../libs/crypto";
import { IIdentifier } from "@veramo/core";
import * as R from "ramda";
import { db } from "../db";
import { useAppStore } from "@/store/app";
import { ref } from "vue";
@Options({
components: {},
@@ -181,72 +188,53 @@ export default class AccountViewView extends Vue {
privateHex = "";
publicHex = "";
UPORT_ROOT_DERIVATION_PATH = "";
async created() {
const previousIdentifiers: Array<IIdentifier> = [];
const toLowercase = true;
source = ref("Hello");
copy = useClipboard().copy;
this.mnemonic = createIdentifier();
[
this.address,
this.privateHex,
this.publicHex,
this.UPORT_ROOT_DERIVATION_PATH,
] = deriveAddress(this.mnemonic);
//appStore.dispatch(appSlice.actions.addLog({log: false, msg: "... derived keys and address..."}))
const prevIds = previousIdentifiers || [];
if (toLowercase) {
const foundEqual = R.find(
(id: IIdentifier) => id.did.split(":")[2] === this.address,
prevIds
// 'created' hook runs when the Vue instance is first created
async created() {
const appCondition = useAppStore().condition;
if (appCondition == "uninitialized") {
this.mnemonic = createIdentifier();
[
this.address,
this.privateHex,
this.publicHex,
this.UPORT_ROOT_DERIVATION_PATH,
] = deriveAddress(this.mnemonic);
const newId = newIdentifier(
this.address,
this.publicHex,
this.privateHex,
this.UPORT_ROOT_DERIVATION_PATH
);
if (foundEqual) {
// appStore.dispatch(appSlice.actions.addLog({log: true, msg: "Will create a normal-case version of the DID since a regular version exists."}))
} else {
this.address = this.address.toLowerCase();
}
} else {
// They're not trying to convert to lowercase.
const foundLower = R.find(
(id: IIdentifier) =>
id.did.split(":")[2] === this.address.toLowerCase(),
prevIds
);
if (foundLower) {
// appStore.dispatch(appSlice.actions.addLog({log: true, msg: "Will create a lowercase version of the DID since a lowercase version exists."}))
this.address = this.address.toLowerCase();
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 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) {
console.log("...");
await db.accounts.add({
publicKey: newId.keys[0].publicKeyHex,
mnemonic: this.mnemonic,
identity: JSON.stringify(newId),
dateCreated: new Date().getTime(),
});
}
await db.open();
const num_accounts = await db.accounts.count();
if (num_accounts === 0) {
console.log("Problem! Should have a profile!");
} else {
const accounts = await db.accounts.toArray();
console.log(accounts[0]);
const identity = JSON.parse(accounts[0].identity);
this.address = identity.did;
this.publicHex = identity.keys[0].publicKeyHex;
//appStore.dispatch(appSlice.actions.addLog({log: false, msg: "... created new ID..."}))
//appStore.dispatch(appSlice.actions.addLog({log: false, msg: "... stored new ID..."}))
} catch (err) {
console.log("Error!");
console.log(err);
this.UPORT_ROOT_DERIVATION_PATH = identity.keys[0].meta.derivationPath;
}
}
}

View File

@@ -19,7 +19,7 @@
<!-- Projects -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link
:to="{ name: 'project' }"
:to="{ name: 'projects' }"
class="block text-center py-3 px-1"
><fa icon="folder-open" class="fa-fw"></fa
></router-link>

View File

@@ -14,42 +14,90 @@
</h1>
</div>
<!-- Import Account Form -->
<form>
<p class="text-center text-xl mb-4 font-light">
Enter your seed phrase below to import your identity on this device.
</p>
<input
type="text"
placeholder="Seed Phrase"
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
/>
<div class="mt-8">
<input
type="submit"
class="block w-full text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md mb-2"
value="Import Identity"
/>
<button
@click="onCancelClick()"
type="button"
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md"
>
Cancel
</button>
</div>
</form>
<p class="text-center text-xl mb-4 font-light">
Enter your seed phrase below to import your identity on this device.
</p>
<input
type="text"
placeholder="Seed Phrase"
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
v-model="mnemonic"
/>
{{ mnemonic }}
<div class="mt-8">
<button
@click="from_mnemonic()"
class="block w-full text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md mb-2"
>
Import
</button>
<button
@click="onCancelClick()"
type="button"
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md"
>
Cancel
</button>
</div>
</section>
</template>
<script lang="ts">
import { Options, Vue } from "vue-class-component";
import { deriveAddress, newIdentifier } from "../libs/crypto";
import { db } from "@/db";
import { useAppStore } from "@/store/app";
@Options({
components: {},
})
export default class ImportAccountView extends Vue {
mnemonic = "";
address = "";
privateHex = "";
publicHex = "";
UPORT_ROOT_DERIVATION_PATH = "";
public onCancelClick() {
this.$router.back();
}
public async from_mnemonic() {
const mne: string = this.mnemonic.trim().toLowerCase();
if (this.mnemonic.trim().length > 0) {
[
this.address,
this.privateHex,
this.publicHex,
this.UPORT_ROOT_DERIVATION_PATH,
] = deriveAddress(mne);
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) {
console.log("...");
await db.accounts.add({
publicKey: newId.keys[0].publicKeyHex,
mnemonic: mne,
identity: JSON.stringify(newId),
dateCreated: new Date().getTime(),
});
}
useAppStore().setCondition("registered");
this.$router.push({ name: "account" });
} catch (err) {
console.log("Error!");
console.log(err);
}
}
}
}
</script>

View File

@@ -10,69 +10,156 @@
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
><fa icon="chevron-left" class="fa-fw"></fa
></router-link>
[New/Edit] Project
</h1>
</div>
<!-- Project Details -->
<form>
<!-- Image - (see design model) Empty -->
<!-- Image - (see design model) Empty -->
<!-- Image - Populated -->
<div class="relative mb-4 rounded-md overflow-hidden">
<div class="absolute top-3 right-3 flex gap-2">
<button
class="text-md font-bold uppercase bg-blue-600 text-white px-3 py-2 rounded"
>
<fa icon="pen" class="fa-fw"></fa>
</button>
<button
class="text-md font-bold uppercase bg-red-600 text-white px-3 py-2 rounded"
>
<fa icon="trash-can" class="fa-fw"></fa>
</button>
</div>
<img src="https://picsum.photos/800/400" class="w-full" />
</div>
<input
type="text"
placeholder="Project Name"
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
/>
<textarea
placeholder="Description"
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
rows="5"
></textarea>
<div class="text-xs text-slate-500 italic -mt-3 mb-4">
88/500 max. characters
</div>
<div class="mt-8">
<input
type="submit"
class="block w-full text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md mb-2"
value="Save Project"
/>
<!-- Image - Populated -->
<div class="relative mb-4 rounded-md overflow-hidden">
<div class="absolute top-3 right-3 flex gap-2">
<button
type="button"
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md"
class="text-md font-bold uppercase bg-blue-600 text-white px-3 py-2 rounded"
>
Cancel
<fa icon="pen" class="fa-fw"></fa>
</button>
<button
class="text-md font-bold uppercase bg-red-600 text-white px-3 py-2 rounded"
>
<fa icon="trash-can" class="fa-fw"></fa>
</button>
</div>
</form>
<img src="https://picsum.photos/800/400" class="w-full" />
</div>
<input
type="text"
placeholder="Project Name"
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
v-model="projectName"
/>
<textarea
placeholder="Description"
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
rows="5"
v-model="description"
></textarea>
<div class="text-xs text-slate-500 italic -mt-3 mb-4">
88/500 max. characters
</div>
<div class="mt-8">
<button
class="block w-full text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md mb-2"
@click="onSaveProjectClick()"
>
Save Project
</button>
<button
type="button"
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md"
@click="onCancelClick()"
>
Cancel
</button>
</div>
</section>
</template>
<script lang="ts">
import { Options, Vue } from "vue-class-component";
import { AppString } from "@/constants/app";
import { db } from "../db";
import { accessToken, SimpleSigner } from "@/libs/crypto";
import * as didJwt from "did-jwt";
import { IIdentifier } from "@veramo/core";
import { useAppStore } from "@/store/app";
@Options({
components: {},
})
export default class NewEditProjectView extends Vue {}
export default class NewEditProjectView extends Vue {
projectName = "";
description = "";
private async SaveProject(identity: IIdentifier) {
const address = identity.did;
// Make a claim
const vcClaim = {
"@context": "https://schema.org",
"@type": "PlanAction",
identifier: address,
name: this.projectName,
description: this.description,
};
// Make a payload for the claim
const vcPayload = {
sub: "PlanAction",
vc: {
"@context": ["https://www.w3.org/2018/credentials/v1"],
type: ["VerifiableCredential"],
credentialSubject: vcClaim,
},
};
// create a signature using private key of identity
if (
identity.keys[0].privateKeyHex !== "undefined" &&
identity.keys[0].privateKeyHex !== null
) {
// eslint-disable-next-line
const privateKeyHex: string = identity.keys[0].privateKeyHex!;
const signer = await SimpleSigner(privateKeyHex);
const alg = undefined;
// create a JWT for the request
const vcJwt: string = await didJwt.createJWT(vcPayload, {
alg: alg,
issuer: identity.did,
signer: signer,
});
// Make the xhr request payload
const payload = JSON.stringify({ jwtEncoded: vcJwt });
const endorserApiServer = AppString.DEFAULT_ENDORSER_API_SERVER;
const url = endorserApiServer + "/api/claim";
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
try {
const resp = await this.axios.post(url, payload, { headers });
console.log(resp.status, resp.data);
useAppStore().setProjectId(resp.data);
const route = {
name: "project",
};
console.log(route);
this.$router.push(route);
} catch (error) {
console.log(error);
}
}
}
public async onSaveProjectClick() {
await db.open();
const num_accounts = await db.accounts.count();
if (num_accounts === 0) {
console.log("Problem! Should have a profile!");
} else {
const accounts = await db.accounts.toArray();
const identity = JSON.parse(accounts[0].identity);
this.SaveProject(identity);
}
}
public onCancelClick() {
this.$router.back();
}
}
</script>

View File

@@ -126,9 +126,16 @@
<script lang="ts">
import { Options, Vue } from "vue-class-component";
import { useAppStore } from "@/store/app";
@Options({
components: {},
})
export default class ProjectViewView extends Vue {}
export default class ProjectViewView extends Vue {
projectId = "";
created(): void {
this.projectId = useAppStore().projectId;
console.log(this.projectId);
}
}
</script>

View File

@@ -1,3 +1,107 @@
<template>
<section id="Content" class="p-6 pb-24"></section>
<section id="Content" class="p-6 pb-24">
<!-- Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
My Projects
</h1>
<!-- Quick Search -->
<form id="QuickSearch" class="mb-4 flex">
<input
type="text"
placeholder="Search…"
class="block w-full rounded-l border-r-0 border-slate-400"
/>
<button
class="px-4 rounded-r bg-slate-200 border border-l-0 border-slate-400"
>
<i class="fa-solid fa-magnifying-glass fa-fw"></i>
</button>
</form>
<!-- New Project -->
<button
class="fixed right-6 bottom-24 text-center text-4xl leading-none bg-blue-600 text-white w-14 py-2.5 rounded-full"
@click="onClickNewProject()"
>
<fa icon="plus" class="fa-fw"></fa>
</button>
<!-- Results List -->
<ul class="">
<li class="border-b border-slate-300">
<a href="project-view.html" class="block py-4 flex gap-4">
<div class="flex-none w-12">
<img
src="https://picsum.photos/200/200?random=1"
class="w-full rounded"
/>
</div>
<div class="grow overflow-hidden">
<h2 class="text-base font-semibold">Canyon cleanup</h2>
<div class="text-sm truncate">
The quick brown fox jumps over the lazy dog. The quick brown fox
jumps over the lazy dog.
</div>
</div>
</a>
</li>
<li class="border-b border-slate-300">
<a href="project-view.html" class="block py-4 flex gap-4">
<div class="flex-none w-12">
<img
src="https://picsum.photos/200/200?random=2"
class="w-full rounded"
/>
</div>
<div class="grow overflow-hidden">
<h2 class="text-base font-semibold">Potluck with neighbors</h2>
<div class="text-sm truncate">
The quick brown fox jumps over the lazy dog. The quick brown fox
jumps over the lazy dog.
</div>
</div>
</a>
</li>
<li class="border-b border-slate-300">
<a href="project-view.html" class="block py-4 flex gap-4">
<div class="flex-none w-12">
<img
src="https://picsum.photos/200/200?random=3"
class="w-full rounded"
/>
</div>
<div class="grow overflow-hidden">
<h2 class="text-base font-semibold">Historical site</h2>
<div class="text-sm truncate">
The quick brown fox jumps over the lazy dog. The quick brown fox
jumps over the lazy dog.
</div>
</div>
</a>
</li>
</ul>
</section>
</template>
<script lang="ts">
import { Options, Vue } from "vue-class-component";
@Options({
components: {},
})
export default class ProjectsView extends Vue {
onClickNewProject(): void {
const route = {
name: "new-edit-project",
};
console.log(route);
this.$router.push(route);
}
}
</script>