Compare commits

...

59 Commits

Author SHA1 Message Date
d48b2210d5 add cypress, but the first test fails with a decrypt message (and besides: app generates new account when visiting /account) 2023-02-16 20:05:37 -07:00
Matthew Aaron Raymer
2d78a46ef2 Added alert box to save project but need a way to trigger an error? 2023-02-16 18:07:19 +08:00
Matthew Aaron Raymer
a2d1569d93 Added Save progress and a text counter on textbox 2023-02-16 17:30:09 +08:00
Jose Olarte III
da6833a0eb Fixed search bar 2023-02-13 18:44:50 +08:00
f3f55e1636 Update 'project.yaml' 2023-02-13 02:12:45 -05:00
6c38e69f9e docs: remove completed tasks regarding IDs 2023-02-07 20:50:56 -07:00
f4dcfb8dad linting update (no functional changes) 2023-02-07 20:30:17 -07:00
Matthew Aaron Raymer
1f114bfc52 Stub for message dialog 2023-02-06 18:51:50 +08:00
1ed22c9848 Update 'project.yaml' 2023-02-03 00:48:12 -05:00
Matthew Aaron Raymer
99c38079b3 More fixes due to typing in catch 2023-02-02 17:51:03 +08:00
Matthew Aaron Raymer
54d556ac4b Fixes to get the linter peachy 2023-02-01 16:55:40 +08:00
c8feb0c35b Update 'project.yaml' 2023-01-31 23:19:16 -05:00
2c74f358c7 Update 'project.yaml' 2023-01-31 23:17:29 -05:00
3f1a0185a4 Update 'project.yaml' 2023-01-31 23:17:02 -05:00
f886be7844 Merge pull request 'update to new endpoints with the editable plan "handle"' (#7) from pull-only-plans into master
Reviewed-on: trent_larson/kick-starter-for-time-pwa#7
2023-01-31 22:46:39 -05:00
83a9dc332c chore: understandable debugging 2023-01-25 16:32:19 -07:00
5638798ca8 chore: update to match latest API for retrieving plans 2023-01-25 09:10:16 -07:00
1bedbe17c0 feat: adjust to the new endpoints for editable plan records 2023-01-23 22:14:46 -07:00
d6253ca737 Merge pull request 'Pull only PlanAction records for this issuer' (#6) from pull-only-plans into master
Reviewed-on: trent_larson/kick-starter-for-time-pwa#6
2023-01-20 00:15:27 -05:00
4664d697fd feat: restrict to only pull PlanActions for issuer 2023-01-19 21:30:00 -07:00
fa01125c84 docs: add testing info, tweak tasks 2023-01-19 21:29:45 -07:00
Matthew Aaron Raymer
68eb04c137 Added updating account name 2023-01-17 16:53:44 +08:00
Matthew Aaron Raymer
51600b65d7 Add edit project flow in anticipation of API method. Ugly Edit button needs to be replaced. 2023-01-17 16:35:38 +08:00
Matthew Aaron Raymer
997093c695 Add flow from project list to view 2023-01-17 16:13:58 +08:00
Matthew Aaron Raymer
cc57d59717 Basic Project list added 2023-01-16 18:35:06 +08:00
Matthew Aaron Raymer
01eecfd8d9 Time since 2023-01-16 17:24:48 +08:00
Matthew Aaron Raymer
64bd9a103d Name and description added to Project View 2023-01-16 16:37:57 +08:00
Matthew Aaron Raymer
c84e597047 Added the flow from New Project to View Project 2023-01-16 14:12:52 +08:00
Matthew Aaron Raymer
c61be23fee Merged in with small corrections 2023-01-10 16:30:03 +08:00
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
8540a2de77 Merge pull request 'Include a way to register other test users' (#4) from test-registration into master
Reviewed-on: trent_larson/kick-starter-for-time-pwa#4
2023-01-09 16:25:34 -05: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
693df1bda1 feat: switch to the SimpleSigner code for working signatures 2023-01-07 23:37:38 -07:00
d3e590822e docs: fix spelling error 2023-01-07 23:34:42 -07:00
4e1263d041 test: add function for test User #0 to register a new user 2023-01-07 23:15:39 -07: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
24 changed files with 2400 additions and 229 deletions

1
.tool-versions Normal file
View File

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

View File

@@ -20,10 +20,54 @@ npm run build
npm run lint
```
### Clear data & restart
Clear cache for localhost, then go to http://localhost:8080/start (because it'll regenerate if you start on the `/account` page).
### Test key contents
See [this page](openssl_signing_console.rst)
### Register new user on test server
New users require registration. This can be done with a claim payload like this by an existing user:
```
const vcClaim = {
"@context": "https://schema.org",
"@type": "RegisterAction",
agent: { did: identity0.did },
object: SERVICE_ID,
participant: { did: newIdentity.did },
};
```
On the test server, User #0 has rights to register others, so you can start playing one of two ways:
- Import the keys for that test User `did:ethr:0x000Ee5654b9742f6Fe18ea970e32b97ee2247B51` by importing this seed phrase: `seminar accuse mystery assist delay law thing deal image undo guard initial shallow wrestle list fragile borrow velvet tomorrow awake explain test offer control`
- Register someone else under User #0 on the `/account` page:
* Edit the `src/views/AccountViewView.vue` file and uncomment the lines referring to "test".
* Use the [Vue Devtools browser extension](https://devtools.vuejs.org/) and type this into the console: `$vm.ctx.testRegisterUser()`
### Create keys with alternate tools
See [this page](openssl_signing_console.rst)
### 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 +166,4 @@ export const createAndStoreIdentifier = async (mnemonicPassword) => {
return importAndStoreIdentifier(mnemonic, mnemonicPassword, false, [])
}
```
```

9
cypress.config.ts Normal file
View File

@@ -0,0 +1,9 @@
import { defineConfig } from "cypress";
export default defineConfig({
e2e: {
setupNodeEvents(on, config) {
// implement node event listeners here
},
},
});

20
cypress/e2e/spec.cy.ts Normal file
View File

@@ -0,0 +1,20 @@
describe('My First Test', () => {
it('finds the content "type"', () => {
cy.visit('http://localhost:8080')
cy.contains('Yes').click()
cy.get('#mnemonic').type('seminar accuse mystery assist delay law thing deal image undo guard initial shallow wrestle list fragile borrow velvet tomorrow awake explain test offer control')
cy.get('#import').click()
cy.get('Share Your ID')
/**
cy.visit('http://localhost:8080/new-edit-project')
cy.get('#name').type('Clicker Shooter')
cy.get('#description').type('Joel\'s new test project via Cypress')
cy.contains('Save Project').click()
**/
})
})

View File

@@ -0,0 +1,5 @@
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io",
"body": "Fixtures are a great way to mock data for responses to routes"
}

View File

@@ -0,0 +1,37 @@
/// <reference types="cypress" />
// ***********************************************
// This example commands.ts shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add('login', (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
//
// declare global {
// namespace Cypress {
// interface Chainable {
// login(email: string, password: string): Chainable<void>
// drag(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
// dismiss(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
// visit(originalFn: CommandOriginalFn, url: string, options: Partial<VisitOptions>): Chainable<Element>
// }
// }
// }

20
cypress/support/e2e.ts Normal file
View File

@@ -0,0 +1,20 @@
// ***********************************************************
// This example support/e2e.ts is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands'
// Alternatively you can use CommonJS syntax:
// require('./commands')

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.

1303
package-lock.json generated

File diff suppressed because it is too large Load Diff

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",
@@ -32,6 +34,7 @@
"localstorage-slim": "^2.3.0",
"luxon": "^3.1.1",
"merkletreejs": "^0.3.9",
"moment": "^2.29.4",
"papaparse": "^5.3.2",
"pina": "^0.20.2204228",
"pinia-plugin-persistedstate": "^3.0.1",
@@ -40,6 +43,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",
@@ -58,6 +62,7 @@
"@vue/cli-service": "~5.0.8",
"@vue/eslint-config-typescript": "^11.0.2",
"autoprefixer": "^10.4.13",
"cypress": "^12.5.1",
"eslint": "^8.28.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-prettier": "^4.2.1",

33
project.yaml Normal file
View File

@@ -0,0 +1,33 @@
- top screens from img/screens.pdf milestone:2 :
- new
- feedback to show saving assignee:matthew status:pending
- edit
- feedback to show saved assignee:matthew status:pending
- view all :
- search bar isn't highlighted & icon on right doesn't show assignee:matthew
- no tab bar across bottom assignee:matthew status:committed
- add spinner assignee:matthew status:pending
- add infinite scroll assignee:matthew
- view one
- image (removed for MVP since we don't have a mechanism for images yet) status:done
blocks: ref:https://raw.githubusercontent.com/trentlarson/lives-of-gifts/master/project.yaml#kickstarter%20for%20time
- replace user-affecting console.logs with error messages (eg. catches)
- contacts
- commit screen
- discover screen
- backup all data
- Next Viable Product afterward
- Connect with phone contacts
- Multiple identities
- Peer DID
- DIDComm

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_API_SERVER = "http://localhost:3000",
}

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);
},
async setProjectId(newProjectId: string) {
localStorage.setItem("projectId", newProjectId);
},
},
});

60
src/test/index.ts Normal file
View File

@@ -0,0 +1,60 @@
import axios from "axios";
import * as didJwt from "did-jwt";
import { AppString } from "@/constants/app";
import { db } from "../db";
import { SERVICE_ID } from "../libs/veramo/setup";
import { deriveAddress, newIdentifier } from "../libs/crypto";
export async function testServerRegisterUser() {
const testUser0Mnem =
"seminar accuse mystery assist delay law thing deal image undo guard initial shallow wrestle list fragile borrow velvet tomorrow awake explain test offer control";
const [addr, privateHex, publicHex, deriPath] = deriveAddress(testUser0Mnem);
const identity0 = newIdentifier(addr, publicHex, privateHex, deriPath);
await db.open();
const accounts = await db.accounts.toArray();
const thisIdentity = JSON.parse(accounts[0].identity);
// Make a claim
const vcClaim = {
"@context": "https://schema.org",
"@type": "RegisterAction",
agent: { did: identity0.did },
object: SERVICE_ID,
participant: { did: thisIdentity.did },
};
// Make a payload for the claim
const vcPayload = {
sub: "RegisterAction",
vc: {
"@context": ["https://www.w3.org/2018/credentials/v1"],
type: ["VerifiableCredential"],
credentialSubject: vcClaim,
},
};
// create a signature using private key of identity
// eslint-disable-next-line
const privateKeyHex: string = identity0.keys[0].privateKeyHex!;
const signer = await didJwt.SimpleSigner(privateKeyHex);
const alg = undefined;
// create a JWT for the request
const vcJwt: string = await didJwt.createJWT(vcPayload, {
alg: alg,
issuer: identity0.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 headers = {
"Content-Type": "application/json",
};
const resp = await axios.post(url, payload, { headers });
console.log("Result:", resp);
}

View File

@@ -72,7 +72,7 @@
<!-- 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>
<h2 class="text-xl font-semibold mb-2">{{ firstName }} {{ lastName }}</h2>
<div class="text-slate-500 text-sm font-bold">ID</div>
<div
@@ -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,86 +173,80 @@
<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";
//import { testServerRegisterUser } from "../test";
@Options({
components: {},
})
export default class AccountViewView extends Vue {
firstName =
localStorage.getItem("firstName") === null
? "--"
: localStorage.getItem("firstName");
lastName =
localStorage.getItem("lastName") === null
? "--"
: localStorage.getItem("lastName");
mnemonic = "";
address = "";
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
// This registers current user in vue plugin with: $vm.ctx.testRegisterUser()
//testRegisterUser = testServerRegisterUser;
// '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,92 @@
</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"
id="mnemonic"
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
id="import"
@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

@@ -18,17 +18,20 @@
type="text"
placeholder="First Name"
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
v-model="firstName"
/>
<input
type="text"
placeholder="Last Name"
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
v-model="lastName"
/>
<div class="mt-8">
<button
type="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="onClickSaveChanges()"
>
Save Changes
</button>
@@ -36,6 +39,7 @@
<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="onClickCancel()"
>
Cancel
</button>
@@ -50,5 +54,27 @@ import { Options, Vue } from "vue-class-component";
@Options({
components: {},
})
export default class NewEditAccountView extends Vue {}
export default class NewEditAccountView extends Vue {
firstName =
localStorage.getItem("firstName") === null
? "--"
: localStorage.getItem("firstName");
lastName =
localStorage.getItem("lastName") === null
? "--"
: localStorage.getItem("lastName");
onClickSaveChanges() {
localStorage.setItem("firstName", this.firstName as string);
localStorage.setItem("lastName", this.lastName as string);
const route = {
name: "account",
};
this.$router.push(route);
}
onClickCancel() {
this.$router.back();
}
}
</script>

View File

@@ -10,69 +10,276 @@
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
[New/Edit] Plan
</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>
<div>
{{ errorMessage }}
</div>
<input
type="text"
placeholder="Project Name"
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
/>
<input
id="name"
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"
></textarea>
<div class="text-xs text-slate-500 italic -mt-3 mb-4">
88/500 max. characters
</div>
<textarea
id="description"
placeholder="Description"
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
rows="5"
v-model="description"
maxlength="500"
></textarea>
<div class="text-xs text-slate-500 italic -mt-3 mb-4">
{{ description.length }}/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"
/>
<button
type="button"
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md"
<div class="mt-8">
<button
:disabled="isHiddenSave"
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()"
>
<!-- SHOW if in idle state -->
<span :class="{ hidden: isHiddenSave }">Save Project</span>
<!-- SHOW if in saving state; DISABLE button while in saving state -->
<span :class="{ hidden: isHiddenSpinner }"
><i class="fa-solid fa-spinner fa-spin-pulse"></i>
Saving&hellip;</span
>
Cancel
</button>
</div>
</form>
</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>
<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()"
>
<i class="fa-solid fa-xmark"></i>
</button>
<h4 class="font-bold pr-5">{{ alertTitle }}</h4>
<p>{{ alertMessage }}</p>
</div>
</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";
import { AxiosError } from "axios";
interface VerifiableCredential {
"@context": string;
"@type": string;
name: string;
description: string;
identifier?: string;
}
@Options({
components: {},
})
export default class NewEditProjectView extends Vue {}
export default class NewEditProjectView extends Vue {
projectName = "";
description = "";
errorMessage = "";
alertTitle = "";
alertMessage = "";
projectId = localStorage.getItem("projectId") || "";
isHiddenSave = false;
isHiddenSpinner = true;
async created() {
if (this.projectId === "") {
console.log("This is a new project");
} else {
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.LoadProject(identity);
}
}
}
async LoadProject(identity: IIdentifier) {
const endorserApiServer = AppString.DEFAULT_ENDORSER_API_SERVER;
// eslint-disable-next-line prettier/prettier
const url = endorserApiServer + "/api/claim/byHandle/" + encodeURIComponent(this.projectId);
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
try {
const resp = await this.axios.get(url, { headers });
console.log(resp.status, resp.data);
if (resp.status === 200) {
const claim = resp.data.claim;
this.projectName = claim.name;
this.description = claim.description;
}
} catch (error) {
console.log(error);
}
}
private async SaveProject(identity: IIdentifier) {
// Make a claim
const vcClaim: VerifiableCredential = {
"@context": "https://schema.org",
"@type": "PlanAction",
name: this.projectName,
description: this.description,
identifier: this.projectId || undefined,
};
if (this.projectId) {
vcClaim.identifier = this.projectId;
}
// 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/v2/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("Got resp data:", resp.data);
if (resp.data?.success?.fullIri) {
this.errorMessage = "";
this.alertTitle = "";
this.alertMessage = "";
useAppStore().setProjectId(resp.data.success.fullIri);
setTimeout(
function (that: Vue) {
const route = {
name: "project",
};
console.log(route);
that.$router.push(route);
},
2000,
this
);
}
} catch (error) {
let userMessage = "There was an error. See logs for more info.";
const serverError = error as AxiosError;
if (serverError) {
this.isAlertVisible = true;
if (serverError.message) {
this.alertTitle = "User Message";
userMessage = serverError.message; // This is info for the user.
this.alertMessage = userMessage;
} else {
this.alertTitle = "Server Message";
this.alertMessage = JSON.stringify(serverError.toJSON());
}
} else {
console.log("Here's the full error trying to save the claim:", error);
this.alertTitle = "Claim Error";
this.alertMessage = error as string;
}
// Now set that error for the user to see.
this.errorMessage = userMessage;
}
}
}
public onClickClose() {
this.isAlertVisible = false;
this.alertTitle = "";
this.alertMessage = "";
}
public async onSaveProjectClick() {
this.isHiddenSave = true;
this.isHiddenSpinner = false;
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();
}
isAlertVisible = false;
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>

View File

@@ -4,27 +4,31 @@
<ul class="flex text-2xl p-2 gap-2">
<!-- Home Feed -->
<li class="basis-1/5 rounded-md text-slate-500">
<a href="" class="block text-center py-3 px-1"
<router-link :to="{ name: 'home' }" class="block text-center py-3 px-1"
><fa icon="house-chimney" class="fa-fw"></fa
></a>
></router-link>
</li>
<!-- Search -->
<li class="basis-1/5 rounded-md text-slate-500">
<a href="search.html" class="block text-center py-3 px-1"
<li class="basis-1/5 rounded-md bg-slate-400 text-white">
<router-link
:to="{ name: 'discover' }"
class="block text-center py-3 px-1"
><fa icon="magnifying-glass" class="fa-fw"></fa
></a>
></router-link>
</li>
<!-- Projects -->
<li class="basis-1/5 rounded-md bg-slate-400 text-white">
<a href="" class="block text-center py-3 px-1"
<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
></a>
></router-link>
</li>
<!-- Commitments -->
<li class="basis-1/5 rounded-md text-slate-500">
<a href="" class="block text-center py-3 px-1"
<router-link :to="{ name: '' }" class="block text-center py-3 px-1"
><fa icon="hand" class="fa-fw"></fa
></a>
></router-link>
</li>
<!-- Profile -->
<li class="basis-1/5 rounded-md text-slate-500">
@@ -56,35 +60,50 @@
><fa icon="ellipsis-vertical" class="fa-fw"></fa
></a>
View Project
View Plan
</h1>
</div>
<div>
{{ errorMessage }}
</div>
<!-- Project Details -->
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
<!-- Image -->
<div class="-mx-4 -mt-3 mb-3">
<img src="https://picsum.photos/800/400" class="w-full" />
</div>
<div>
<h2 class="text-xl font-semibold">Canyon cleanup</h2>
<h2 class="text-xl font-semibold">{{ name }}</h2>
<div class="flex justify-between gap-4 text-sm mb-3">
<span><fa icon="user" class="fa-fw text-slate-400"></fa> Rotary</span>
<span
><fa icon="calendar" class="fa-fw text-slate-400"></fa> 8 days
ago</span
>
><fa icon="calendar" class="fa-fw text-slate-400"></fa
>{{ timeSince }}
</span>
</div>
<div class="text-sm text-slate-500">
Sed ut perspiciatis unde omnis iste natus error sit voluptatem
accusantium doloremque laudantium
<a href="" class="uppercase text-xs font-semibold text-slate-700"
>Read More</a
>
<div v-if="!expanded">
{{ truncatedDesc }}
<a v-if="description.length >= truncateLength" @click="expandText"
>Read More</a
>
</div>
<div v-else>
{{ description }}
<a
@click="collapseText"
class="uppercase text-xs font-semibold text-slate-700"
>Read Less</a
>
</div>
</div>
</div>
<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="onEditClick()"
>
Edit
</button>
</div>
<!-- Commit -->
@@ -126,9 +145,93 @@
<script lang="ts">
import { Options, Vue } from "vue-class-component";
import { accessToken } from "@/libs/crypto";
import { db } from "../db";
import { IIdentifier } from "@veramo/core";
import { AppString } from "@/constants/app";
import * as moment from "moment";
import { AxiosError } from "axios";
@Options({
components: {},
})
export default class ProjectViewView extends Vue {}
export default class ProjectViewView extends Vue {
expanded = false;
name = "";
description = "";
truncatedDesc = "";
truncateLength = 40;
timeSince = "";
projectId = localStorage.getItem("projectId") || "";
errorMessage = "";
onEditClick() {
localStorage.setItem("projectId", this.projectId as string);
const route = {
name: "new-edit-project",
};
console.log(route);
this.$router.push(route);
}
expandText() {
this.expanded = true;
}
collapseText() {
this.expanded = false;
}
async LoadProject(identity: IIdentifier) {
const endorserApiServer = AppString.DEFAULT_ENDORSER_API_SERVER;
// eslint-disable-next-line prettier/prettier
const url = endorserApiServer + "/api/claim/byHandle/" + encodeURIComponent(this.projectId);
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
try {
const resp = await this.axios.get(url, { headers });
console.log("resp.status, resp.data", resp.status, resp.data);
if (resp.status === 200) {
const startTime = resp.data.startTime;
if (startTime != null) {
const eventDate = new Date(startTime);
const now = moment.now();
this.timeSince = moment.utc(now).to(eventDate);
}
this.name = resp.data.claim?.name || "(no name)";
this.description = resp.data.claim?.description || "(no description)";
this.truncatedDesc = this.description.slice(0, this.truncateLength);
} else if (resp.status === 404) {
// actually, axios throws an error so we never get here
this.errorMessage = "That project does not exist.";
}
} catch (error: unknown) {
const serverError = error as AxiosError;
if (serverError.response?.status === 404) {
this.errorMessage = "That project does not exist.";
} else {
this.errorMessage =
"Something went wrong retrieving that project." +
" See logs for more info.";
console.log("Error retrieving project:", error);
}
}
}
async created() {
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.LoadProject(identity);
}
}
}
</script>

View File

@@ -1,3 +1,173 @@
<template>
<section id="Content" class="p-6 pb-24"></section>
<!-- QUICK NAV -->
<nav id="QuickNav" class="fixed bottom-0 left-0 right-0 bg-slate-200">
<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 bg-slate-400 text-white">
<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>
<!-- Commitments -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link :to="{ name: '' }" class="block text-center py-3 px-1"
><fa icon="hand" class="fa-fw"></fa
></router-link>
</li>
<!-- Profile -->
<li class="basis-1/5 rounded-md text-slate-500">
<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>
<section id="Content" class="p-6 pb-24">
<!-- Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
My Plans
</h1>
<!-- Quick Search -->
<form id="QuickSearch" class="mb-4 flex">
<input
type="text"
placeholder="Search…"
class="block w-full rounded-l border border-r-0 border-slate-400 px-3 py-2"
/>
<button
class="px-4 rounded-r bg-slate-200 border border-l-0 border-slate-400"
>
<fa icon="magnifying-glass" class="fa-fw"></fa>
</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"
v-for="project in projects"
:key="project.handleId"
>
<a
@click="onClickLoadProject(project.handleId)"
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">{{ project.name }}</h2>
<div class="text-sm truncate">
{{ project.description }}
</div>
</div>
</a>
</li>
</ul>
</section>
</template>
<script lang="ts">
import { Options, Vue } from "vue-class-component";
import { accessToken } from "@/libs/crypto";
import { db } from "../db";
import { IIdentifier } from "@veramo/core";
import { AppString } from "@/constants/app";
@Options({
components: {},
})
export default class ProjectsView extends Vue {
projects: { handleId: string; name: string; description: string }[] = [];
onClickLoadProject(id: string) {
console.log("projectId", id);
localStorage.setItem("projectId", id);
const route = {
name: "project",
};
console.log(route);
this.$router.push(route);
}
async LoadProjects(identity: IIdentifier) {
const endorserApiServer = AppString.DEFAULT_ENDORSER_API_SERVER;
const url = endorserApiServer + "/api/v2/report/plansByIssuer";
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
try {
const resp = await this.axios.get(url, { headers });
if (resp.status === 200) {
const plans = resp.data.data;
for (let i = 0; i < plans.length; i++) {
const plan = plans[i];
const data = {
name: plan.name,
description: plan.description,
handleId: plan.fullIri,
};
this.projects.push(data);
}
}
} catch (error) {
console.log(error);
}
}
async created() {
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.LoadProjects(identity);
}
}
onClickNewProject(): void {
localStorage.removeItem("projectId");
const route = {
name: "new-edit-project",
};
console.log(route);
this.$router.push(route);
}
}
</script>