Compare commits

..

90 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
Matthew Aaron Raymer
05e969fbc4 Some small cleanup. Tried Plugin and have abandoned. 2022-12-17 18:28:53 +08:00
Matthew Aaron Raymer
4a407b43ae Fix encrypt-decrypt issue. 2022-12-17 13:32:00 +08:00
Matthew Aaron Raymer
c6d0473fab Refactored again. Fields update on Account page 2022-12-16 18:46:01 +08:00
Matthew Aaron Raymer
607230b51c Cleaned up old Dexie class. Added mnemonic and dateCreated columns. 2022-12-16 13:34:01 +08:00
Matthew Aaron Raymer
c9d5ab82fd Adds an initial account as long as there are no available accounts. 2022-12-15 17:52:42 +08:00
Matthew Aaron Raymer
3ac8f911ac DB being created. Still getting an exception. 2022-12-15 16:45:38 +08:00
Matthew Aaron Raymer
9232afb5af In transition ... experimenting 2022-12-15 16:36:23 +08:00
Matthew Aaron Raymer
2c57bbf4ee Added a source map to aid in debugging 2022-12-14 17:34:53 +08:00
Matthew Aaron Raymer
c239906a96 New structure 2022-12-14 17:06:16 +08:00
Matthew Aaron Raymer
0fa0936c59 Clean up a bit 2022-12-13 18:00:40 +08:00
Matthew Aaron Raymer
aad6b8273b Stil fixing database writes 2022-12-13 17:47:04 +08:00
Matthew Aaron Raymer
571fd241aa Added Dixie class and cryptography. Need to figure out generating password, storing, and boot sequence. 2022-12-12 18:35:20 +08:00
Matthew Aaron Raymer
e0a3f92211 Add Dixie and make some dependency updates 2022-12-12 14:28:58 +08:00
Matthew Aaron Raymer
b58c0ce820 First iteration of account creation. More refactoring to come 2022-12-09 18:23:53 +08:00
Matthew Aaron Raymer
cbad2e7308 Initial foray into the enrolling. Expect refactoring. 2022-12-08 17:36:26 +08:00
Matthew Aaron Raymer
84af5287de Fix to new routing rules 2022-12-08 15:37:07 +08:00
Matthew Aaron Raymer
39f2d73007 Adding new routing logic ... broken for the moment 2022-12-07 17:59:37 +08:00
Matthew Aaron Raymer
ed23317b0f Adding an appStore Pinia datastore 2022-12-05 17:50:28 +08:00
Matthew Aaron Raymer
3c843b2f16 Add a back-button action 2022-12-05 16:54:40 +08:00
Matthew Aaron Raymer
617de58a92 Change to anchors with click handlers so we can run code before transitioning to next pages 2022-12-05 16:52:28 +08:00
Matthew Aaron Raymer
290a13fbf2 Fixing up some dependencies 2022-12-05 16:10:29 +08:00
Matthew Aaron Raymer
5c14275a75 Added Pinia account state tracking. Also modified router to go to Start if no account is defined. 2022-12-02 18:54:47 +08:00
Matthew Aaron Raymer
3c388da22d Added Pinia in addition to Vuex for state-management. Will migrate to Pinia eventually 2022-12-01 17:43:43 +08:00
Matthew Aaron Raymer
ba143dfccd Cleanup of some workflow actions and quick nav 2022-12-01 16:16:58 +08:00
Matthew Aaron Raymer
037fb09d82 A few small fixes and adding some possible dependencies we'll need later 2022-11-30 16:36:21 +08:00
Matthew Aaron Raymer
3357ee08eb Updates to quick navigation 2022-11-30 15:40:10 +08:00
Matthew Aaron Raymer
65d4efb936 Updates to add back buttons and known router paths 2022-11-30 14:39:24 +08:00
Matthew Aaron Raymer
f28e2123b1 Resynching master branch 2022-11-30 13:20:45 +08:00
Matthew Aaron Raymer
301b96ef3a Merge branch 'tmp' 2022-11-27 14:18:18 +08:00
Matthew Aaron Raymer
7cb2821e76 FontAwesome added 2022-11-27 13:44:12 +08:00
Matthew Aaron Raymer
7d707e47f4 Clean up vestigal boilerplat 2022-11-27 13:30:21 +08:00
38 changed files with 16611 additions and 632 deletions

View File

@@ -15,5 +15,6 @@ module.exports = {
rules: {
"no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
"no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",
"@typescript-eslint/no-unnecessary-type-constraint": "off",
},
};

1
.tool-versions Normal file
View File

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

145
README.md
View File

@@ -20,5 +20,150 @@ 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>) => {
// just to get rid of variability that might cause an error
mnemonic = mnemonic.trim().toLowerCase()
/**
// an approach I pieced together
// requires: yarn add elliptic
// ... plus:
// const EC = require('elliptic').ec
// const secp256k1 = new EC('secp256k1')
//
const keyHex: string = bip39.mnemonicToEntropy(mnemonic)
// returns a KeyPair from the elliptic.ec library
const keyPair = secp256k1.keyFromPrivate(keyHex, 'hex')
// this code is from did-provider-eth createIdentifier
const privateHex = keyPair.getPrivate('hex')
const publicHex = keyPair.getPublic('hex')
const address = didJwt.toEthereumAddress(publicHex)
**/
/**
// from https://github.com/uport-project/veramo/discussions/346#discussioncomment-302234
// ... which almost works but the didJwt.toEthereumAddress is wrong
// requires: yarn add bip32
// ... plus: import * as bip32 from 'bip32'
//
const seed: Buffer = await bip39.mnemonicToSeed(mnemonic)
const root = bip32.fromSeed(seed)
const node = root.derivePath(UPORT_ROOT_DERIVATION_PATH)
const privateHex = node.privateKey.toString("hex")
const publicHex = node.publicKey.toString("hex")
const address = didJwt.toEthereumAddress('0x' + publicHex)
**/
/**
// from https://github.com/uport-project/veramo/discussions/346#discussioncomment-302234
// requires: yarn add @ethersproject/hdnode
// ... plus: import { HDNode } from '@ethersproject/hdnode'
**/
const hdnode: HDNode = HDNode.fromMnemonic(mnemonic)
const rootNode: HDNode = hdnode.derivePath(UPORT_ROOT_DERIVATION_PATH)
const privateHex = rootNode.privateKey.substring(2) // original starts with '0x'
const publicHex = rootNode.publicKey.substring(2) // original starts with '0x'
let address = rootNode.address
const prevIds = previousIdentifiers || [];
if (toLowercase) {
const foundEqual = R.find(
(id) => utility.rawAddressOfDid(id.did) === address,
prevIds
)
if (foundEqual) {
// They're trying to create a lowercase version of one that exists in normal case.
// (We really should notify the user.)
appStore.dispatch(appSlice.actions.addLog({log: true, msg: "Will create a normal-case version of the DID since a regular version exists."}))
} else {
address = address.toLowerCase()
}
} else {
// They're not trying to convert to lowercase.
const foundLower = R.find((id) =>
utility.rawAddressOfDid(id.did) === address.toLowerCase(),
prevIds
)
if (foundLower) {
// They're trying to create a normal case version of one that exists in lowercase.
// (We really should notify the user.)
appStore.dispatch(appSlice.actions.addLog({log: true, msg: "Will create a lowercase version of the DID since a lowercase version exists."}))
address = address.toLowerCase()
}
}
appStore.dispatch(appSlice.actions.addLog({log: false, msg: "... derived keys and address..."}))
const newId = newIdentifier(address, publicHex, privateHex, UPORT_ROOT_DERIVATION_PATH)
appStore.dispatch(appSlice.actions.addLog({log: false, msg: "... created new ID..."}))
// awaiting because otherwise the UI may not see that a mnemonic was created
const savedId = await storeIdentifier(newId, mnemonic, mnemonicPassword)
appStore.dispatch(appSlice.actions.addLog({log: false, msg: "... stored new ID..."}))
return savedId
}
// Create a totally new ID
export const createAndStoreIdentifier = async (mnemonicPassword) => {
// This doesn't give us the entropy/seed.
//const id = await agent.didManagerCreate()
const entropy = crypto.randomBytes(32)
const mnemonic = bip39.entropyToMnemonic(entropy)
appStore.dispatch(appSlice.actions.addLog({log: false, msg: "... generated mnemonic..."}))
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.

15227
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -8,17 +8,49 @@
"lint": "vue-cli-service lint"
},
"dependencies": {
"@ethersproject/hdnode": "^5.7.0",
"@fortawesome/fontawesome-svg-core": "^6.2.1",
"@fortawesome/free-solid-svg-icons": "^6.2.1",
"@fortawesome/vue-fontawesome": "^3.0.2",
"@pvermeer/dexie-encrypted-addon": "^2.0.2",
"@veramo/core": "^4.1.1",
"@veramo/credential-w3c": "^4.1.1",
"@veramo/data-store": "^4.1.1",
"@veramo/did-manager": "^4.1.1",
"@veramo/did-provider-ethr": "^4.1.2",
"@veramo/did-resolver": "^4.1.1",
"@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",
"js-generate-password": "^0.1.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",
"ramda": "^0.28.0",
"readable-stream": "^4.2.0",
"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",
"vuex": "^4.1.0"
"web-did-resolver": "^2.0.21"
},
"devDependencies": {
"@types/ramda": "^0.28.20",
"@typescript-eslint/eslint-plugin": "^5.44.0",
"@typescript-eslint/parser": "^5.44.0",
"@vue/cli-plugin-babel": "~5.0.8",
@@ -30,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

@@ -3,3 +3,5 @@
</template>
<style></style>
<script lang="ts"></script>

9
src/constants/app.ts Normal file
View File

@@ -0,0 +1,9 @@
/**
* Generic strings that could be used throughout the app.
*/
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",
}

32
src/db/index.ts Normal file
View File

@@ -0,0 +1,32 @@
import BaseDexie from "dexie";
import { encrypted, Encryption } from "@pvermeer/dexie-encrypted-addon";
import { accountsSchema, AccountsTable } from "./tables/accounts";
/**
* In order to make the next line be acceptable, the program needs to have its linter suppress a rule:
* https://typescript-eslint.io/rules/no-unnecessary-type-constraint/
*
* and change *any* to *unknown*
*
* https://9to5answer.com/how-to-bypass-warning-unexpected-any-specify-a-different-type-typescript-eslint-no-explicit-any
*/
type DexieTables = AccountsTable;
export type Dexie<T extends unknown = DexieTables> = BaseDexie & T;
export const db = new BaseDexie("kickStarter") as Dexie;
const schema = Object.assign({}, accountsSchema);
/**
* Needed to enable a special webpack setting to allow *await* below:
* https://stackoverflow.com/questions/72474803/error-the-top-level-await-experiment-is-not-enabled-set-experiments-toplevelaw
*/
// create password and place password in localStorage
const secret =
localStorage.getItem("secret") || Encryption.createRandomEncryptionKey();
if (localStorage.getItem("secret") == null) {
localStorage.setItem("secret", secret);
}
console.log(secret);
encrypted(db, { secretKey: secret });
db.version(1).stores(schema);

18
src/db/tables/accounts.ts Normal file
View File

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

150
src/libs/crypto/index.ts Normal file
View File

@@ -0,0 +1,150 @@
import { IIdentifier } from "@veramo/core";
import { DEFAULT_DID_PROVIDER_NAME } from "../veramo/setup";
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";
/**
*
*
* @param {string} address
* @param {string} publicHex
* @param {string} privateHex
* @param {string} derivationPath
* @return {*} {Omit<IIdentifier, 'provider'>}
*/
export const newIdentifier = (
address: string,
publicHex: string,
privateHex: string,
derivationPath: string
): Omit<IIdentifier, keyof "provider"> => {
return {
did: DEFAULT_DID_PROVIDER_NAME + ":" + address,
keys: [
{
kid: publicHex,
kms: "local",
meta: { derivationPath: derivationPath },
privateKeyHex: privateHex,
publicKeyHex: publicHex,
type: "Secp256k1",
},
],
provider: DEFAULT_DID_PROVIDER_NAME,
services: [],
};
};
/**
*
*
* @param {string} mnemonic
* @return {*} {[string, string, string, string]}
*/
export const deriveAddress = (
mnemonic: string
): [string, string, string, string] => {
const UPORT_ROOT_DERIVATION_PATH = "m/7696500'/0'/0'/0'";
mnemonic = mnemonic.trim().toLowerCase();
const hdnode: HDNode = HDNode.fromMnemonic(mnemonic);
const rootNode: HDNode = hdnode.derivePath(UPORT_ROOT_DERIVATION_PATH);
const privateHex = rootNode.privateKey.substring(2); // original starts with '0x'
const publicHex = rootNode.publicKey.substring(2); // original starts with '0x'
const address = rootNode.address;
return [address, privateHex, publicHex, UPORT_ROOT_DERIVATION_PATH];
};
/**
*
*
* @return {*} {string}
*/
export const createIdentifier = (): string => {
const entropy: Uint8Array = getRandomBytesSync(32);
const mnemonic = entropyToMnemonic(entropy, wordlist);
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");
}

100
src/libs/veramo/appSlice.ts Normal file
View File

@@ -0,0 +1,100 @@
/* import * as R from "ramda";
import { configureStore, createSlice } from "@reduxjs/toolkit";
import { IIdentifier } from "@veramo/core";
import { Contact } from "../entity/contact";
import { Settings } from "../entity/settings";
import * as utility from "../utility/utility";
const MAX_LOG_LENGTH = 2000000;
export const DEFAULT_ENDORSER_API_SERVER = "https://endorser.ch:3000";
export const DEFAULT_ENDORSER_VIEW_SERVER = "https://endorser.ch";
export const LOCAL_ENDORSER_API_SERVER = "http://127.0.0.1:3000";
export const LOCAL_ENDORSER_VIEW_SERVER = "http://127.0.0.1:3001";
export const TEST_ENDORSER_API_SERVER = "https://test.endorser.ch:8000";
export const TEST_ENDORSER_VIEW_SERVER = "https://test.endorser.ch:8080";
// for contents set in reducers
interface Payload<T> {
type: string;
payload: T;
}
interface LogMsg {
log: boolean;
msg: string;
}
export const appSlice = createSlice({
name: "app",
initialState: {
// This is nullable because it is cached state from the DB...
// it'll be null if we haven't even loaded from the DB yet.
settings: null as Settings,
// This is nullable because it is cached state from the DB...
// it'll be null if we haven't even loaded from the DB yet.
identifiers: null as Array<IIdentifier> | null,
// This is nullable because it is cached state from the DB...
// it'll be null if we haven't even loaded from the DB yet.
contacts: null as Array<Contact> | null,
viewServer: DEFAULT_ENDORSER_VIEW_SERVER,
logMessage: "",
advancedMode: false,
testMode: false,
},
reducers: {
addIdentifier: (state, contents: Payload<IIdentifier>) => {
state.identifiers = state.identifiers.concat([contents.payload]);
},
addLog: (state, contents: Payload<LogMsg>) => {
if (state.logMessage.length > MAX_LOG_LENGTH) {
state.logMessage =
"<truncated>\n..." +
state.logMessage.substring(
state.logMessage.length - MAX_LOG_LENGTH / 2
);
}
if (contents.payload.log) {
console.log(contents.payload.msg);
state.logMessage += "\n" + contents.payload.msg;
}
},
setAdvancedMode: (state, contents: Payload<boolean>) => {
state.advancedMode = contents.payload;
},
setContacts: (state, contents: Payload<Array<Contact>>) => {
state.contacts = contents.payload;
},
setContact: (state, contents: Payload<Contact>) => {
const index = R.findIndex(
(c) => c.did === contents.payload.did,
state.contacts
);
state.contacts[index] = contents.payload;
},
setHomeScreen: (state, contents: Payload<string>) => {
state.settings.homeScreen = contents.payload;
},
setIdentifiers: (state, contents: Payload<Array<IIdentifier>>) => {
state.identifiers = contents.payload;
},
setSettings: (state, contents: Payload<Settings>) => {
state.settings = contents.payload;
},
setTestMode: (state, contents: Payload<boolean>) => {
state.testMode = contents.payload;
},
setViewServer: (state, contents: Payload<string>) => {
state.viewServer = contents.payload;
},
},
});
export const appStore = configureStore({ reducer: appSlice.reducer });
*/

151
src/libs/veramo/setup.ts Normal file
View File

@@ -0,0 +1,151 @@
// Created from the setup in https://veramo.io/docs/guides/react_native
// Core interfaces
/* import {
createAgent,
IDIDManager,
IResolver,
IDataStore,
IKeyManager,
} from "@veramo/core";
*/
// Core identity manager plugin
//import { DIDManager } from "@veramo/did-manager";
// Ethr did identity provider
//import { EthrDIDProvider } from "@veramo/did-provider-ethr";
// Core key manager plugin
//import { KeyManager } from "@veramo/key-manager";
// Custom key management system for RN
//import { KeyManagementSystem } from '@veramo/kms-local-react-native'
// Custom resolver
// Custom resolvers
//import { DIDResolverPlugin } from "@veramo/did-resolver";
/* import { Resolver } from "did-resolver";
import { getResolver as ethrDidResolver } from "ethr-did-resolver";
import { getResolver as webDidResolver } from "web-did-resolver";
*/
// for VCs and VPs https://veramo.io/docs/api/credential-w3c
//import { CredentialIssuer } from '@veramo/credential-w3c'
// Storage plugin using TypeOrm
/* import {
Entities,
KeyStore,
DIDStore,
IDataStoreORM,
} from "@veramo/data-store";
*/
// TypeORM is installed with @veramo/typeorm
//import { createConnection } from 'typeorm'
//import * as R from "ramda";
/*
import { Contact } from '../entity/contact'
import { Settings } from '../entity/settings'
import { PrivateData } from '../entity/privateData'
import { Initial1616938713828 } from '../migration/1616938713828-initial'
import { SettingsContacts1616967972293 } from '../migration/1616967972293-settings-contacts'
import { EncryptedSeed1637856484788 } from '../migration/1637856484788-EncryptedSeed'
import { HomeScreenConfig1639947962124 } from '../migration/1639947962124-HomeScreenConfig'
import { HandlePublicKeys1652142819353 } from '../migration/1652142819353-HandlePublicKeys'
import { LastClaimsSeen1656811846836 } from '../migration/1656811846836-LastClaimsSeen'
import { ContactRegistered1662256903367 }from '../migration/1662256903367-ContactRegistered'
import { PrivateData1663080623479 } from '../migration/1663080623479-PrivateData'
const ALL_ENTITIES = Entities.concat([Contact, Settings, PrivateData])
// Create react native DB connection configured by ormconfig.js
export const dbConnection = createConnection({
database: 'endorser-mobile.sqlite',
entities: ALL_ENTITIES,
location: 'default',
logging: ['error', 'info', 'warn'],
migrations: [ Initial1616938713828, SettingsContacts1616967972293, EncryptedSeed1637856484788, HomeScreenConfig1639947962124, HandlePublicKeys1652142819353, LastClaimsSeen1656811846836, ContactRegistered1662256903367, PrivateData1663080623479 ],
migrationsRun: true,
type: 'react-native',
})
*/
function didProviderName(netName: string) {
return "did:ethr" + (netName === "mainnet" ? "" : ":" + netName);
}
//const NETWORK_NAMES = ["mainnet", "rinkeby"];
const DEFAULT_DID_PROVIDER_NETWORK_NAME = "mainnet";
export const DEFAULT_DID_PROVIDER_NAME = didProviderName(
DEFAULT_DID_PROVIDER_NETWORK_NAME
);
export const HANDY_APP = false;
// this is used as the object in RegisterAction claims
export const SERVICE_ID = "endorser.ch";
//const INFURA_PROJECT_ID = "INFURA_PROJECT_ID";
/*
const providers = {}
NETWORK_NAMES.forEach((networkName) => {
providers[didProviderName(networkName)] = new EthrDIDProvider({
defaultKms: 'local',
network: networkName,
rpcUrl: 'https://' + networkName + '.infura.io/v3/' + INFURA_PROJECT_ID,
gas: 1000001,
ttl: 60 * 60 * 24 * 30 * 12 + 1,
})
})
const didManager = new DIDManager({
store: new DIDStore(dbConnection),
defaultProvider: DEFAULT_DID_PROVIDER_NAME,
providers: providers,
})
*/
/* const basicDidResolvers = NETWORK_NAMES.map((networkName) => [
networkName,
new Resolver({
ethr: ethrDidResolver({
networks: [
{
name: networkName,
rpcUrl:
"https://" + networkName + ".infura.io/v3/" + INFURA_PROJECT_ID,
},
],
}).ethr,
web: webDidResolver().web,
}),
]);
const basicResolverMap = R.fromPairs(basicDidResolvers)
export const DEFAULT_BASIC_RESOLVER = basicResolverMap[DEFAULT_DID_PROVIDER_NETWORK_NAME]
const agentDidResolvers = NETWORK_NAMES.map((networkName) => {
return new DIDResolverPlugin({
resolver: basicResolverMap[networkName],
})
})
let allPlugins = [
new CredentialIssuer(),
new KeyManager({
store: new KeyStore(dbConnection),
kms: {
local: new KeyManagementSystem(),
},
}),
didManager,
].concat(agentDidResolvers)
*/
//export const agent = createAgent<IDIDManager & IKeyManager & IDataStore & IDataStoreORM & IResolver>({ plugins: allPlugins })

View File

@@ -1,8 +1,10 @@
import { createPinia } from "pinia";
import { createApp } from "vue";
import App from "./App.vue";
import "./registerServiceWorker";
import router from "./router";
import store from "./store";
import axios from "axios";
import VueAxios from "vue-axios";
import "./assets/styles/tailwind.css";
@@ -19,6 +21,7 @@ import {
faQrcode,
faUser,
faPen,
faPlus,
faTrashCan,
faCalendar,
faEllipsisVertical,
@@ -38,6 +41,7 @@ library.add(
faQrcode,
faUser,
faPen,
faPlus,
faTrashCan,
faCalendar,
faEllipsisVertical,
@@ -49,6 +53,7 @@ import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
createApp(App)
.component("fa", FontAwesomeIcon)
.use(store)
.use(createPinia())
.use(VueAxios, axios)
.use(router)
.mount("#app");

View File

@@ -1,18 +1,25 @@
import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router";
import HomeView from "../views/HomeView.vue";
import { useAppStore } from "../store/app";
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"),
},
@@ -74,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,13 +96,22 @@ const routes: Array<RouteRecordRaw> = [
),
},
{
path: "/project",
name: "project",
path: "/projects",
name: "projects",
component: () =>
import(/* webpackChunkName: "project" */ "../views/ProjectViewView.vue"),
import(/* webpackChunkName: "projects" */ "../views/ProjectsView.vue"),
},
{
path: "/commitments",
name: "commitments",
component: () =>
import(
/* webpackChunkName: "commitments" */ "../views/CommitmentsView.vue"
),
},
];
/** @type {*} */
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes,

22
src/store/account.ts Normal file
View File

@@ -0,0 +1,22 @@
// @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");
},
},
});

35
src/store/app.ts Normal file
View File

@@ -0,0 +1,35 @@
// @ts-check
import { defineStore } from "pinia";
export const useAppStore = defineStore({
id: "app",
state: () => ({
_condition:
typeof localStorage["condition"] == "undefined"
? "uninitialized"
: localStorage["condition"],
_lastView:
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);
},
},
});

View File

@@ -1,9 +0,0 @@
import { createStore } from "vuex";
export default createStore({
state: {},
getters: {},
mutations: {},
actions: {},
modules: {},
});

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

@@ -4,33 +4,45 @@
<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"
><fa icon="house-chimney" class="fa-fw"></fa
></a>
<router-link :to="{ name: 'home' }" class="block text-center py-3 px-1">
<fa icon="house-chimney" class="fa-fw"></fa>
</router-link>
</li>
<!-- Search -->
<li class="basis-1/5 rounded-md text-slate-500">
<a href="search.html" class="block text-center py-3 px-1"
><fa icon="magnifying-glass" class="fa-fw"></fa
></a>
<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">
<a href="" class="block text-center py-3 px-1"
><fa icon="folder-open" class="fa-fw"></fa
></a>
<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">
<a href="" class="block text-center py-3 px-1"
><fa icon="hand" class="fa-fw"></fa
></a>
<router-link
:to="{ name: 'commitments' }"
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 bg-slate-400 text-white">
<a href="account-view.html" class="block text-center py-3 px-1"
><fa icon="circle-user" class="fa-fw"></fa
></a>
<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>
@@ -60,16 +72,18 @@
<!-- 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
class="text-sm text-slate-500 flex justify-between items-center mb-1"
>
<span
><code>did:peer:kl45kj41lk451kl3</code>
<fa icon="copy" class="text-slate-400 fa-fw ml-1"></fa
></span>
><code>{{ address }}</code>
<button @click="copy(address)">
<fa icon="copy" class="text-slate-400 fa-fw ml-1"></fa>
</button>
</span>
<span>
<button
class="text-xs uppercase bg-slate-500 text-white px-1.5 py-1 rounded-md"
@@ -87,24 +101,28 @@
<div class="text-slate-500 text-sm font-bold">Public Key</div>
<div class="text-sm text-slate-500 mb-1">
<span
><code>dyIgKepL19trfrFu5jzkoNhI</code>
<fa icon="copy" class="text-slate-400 fa-fw ml-1"></fa
></span>
><code>{{ publicHex }}</code>
<button @click="copy(publicHex)">
<fa icon="copy" class="text-slate-400 fa-fw ml-1"></fa>
</button>
</span>
</div>
<div class="text-slate-500 text-sm font-bold">Derivation Path</div>
<div class="text-sm text-slate-500 mb-1">
<span
><code>m/44'/0'/0'/0/0</code>
<fa icon="copy" class="text-slate-400 fa-fw ml-1"></fa
></span>
><code>{{ UPORT_ROOT_DERIVATION_PATH }}</code>
<button @click="copy(UPORT_ROOT_DERIVATION_PATH)">
<fa icon="copy" class="text-slate-400 fa-fw ml-1"></fa>
</button>
</span>
</div>
</div>
<a
href="account-edit.html"
<router-link
:to="{ name: 'new-edit-account' }"
class="block text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md mb-8"
>Edit Identity</a
>Edit Identity</router-link
>
<h3 class="text-sm uppercase font-semibold mb-3">Contact Actions</h3>
@@ -155,9 +173,81 @@
<script lang="ts">
import { Options, Vue } from "vue-class-component";
import { useClipboard } from "@vueuse/core";
import { createIdentifier, deriveAddress, newIdentifier } from "../libs/crypto";
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 {}
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 = "";
source = ref("Hello");
copy = useClipboard().copy;
// 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
);
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);
}
}
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.address = identity.did;
this.publicHex = identity.keys[0].publicKeyHex;
this.UPORT_ROOT_DERIVATION_PATH = identity.keys[0].meta.derivationPath;
}
}
}
</script>

View File

@@ -0,0 +1,3 @@
<template>
<section id="Content" class="p-6 pb-24"></section>
</template>

View File

@@ -5,11 +5,11 @@
<div id="ViewBreadcrumb" class="mb-8">
<h1 class="text-lg text-center font-light relative px-7">
<!-- Cancel -->
<a
href="account-view.html"
<router-link
:to="{ name: 'account' }"
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
><fa icon="chevron-left" class="fa-fw"></fa
></a>
></router-link>
Confirm Contact
</h1>

View File

@@ -4,11 +4,11 @@
<div id="ViewBreadcrumb" class="mb-8">
<h1 class="text-lg text-center font-light relative px-7">
<!-- Cancel -->
<a
href="account-view.html"
<router-link
:to="{ name: 'account' }"
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
><fa icon="chevron-left" class="fa-fw"></fa
></a>
></router-link>
Scan Contact
</h1>

View File

@@ -4,33 +4,39 @@
<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 bg-slate-400 text-white">
<a href="search.html" class="block text-center py-3 px-1"
<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 text-slate-500">
<a href="" class="block text-center py-3 px-1"
<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">
<a href="account-view.html" class="block text-center py-3 px-1"
<router-link
:to="{ name: 'account' }"
class="block text-center py-3 px-1"
><fa icon="circle-user" class="fa-fw"></fa
></a>
></router-link>
</li>
</ul>
</nav>

View File

@@ -4,46 +4,102 @@
<div id="ViewBreadcrumb" class="mb-8">
<h1 class="text-lg text-center font-light relative px-7">
<!-- Cancel -->
<a
href="start.html"
<button
@click="$router.go(-1)"
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
><fa icon="chevron-left"></fa>
</a>
>
<fa icon="chevron-left"></fa>
</button>
Import Existing Identity
</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
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 {}
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

@@ -4,11 +4,12 @@
<div id="ViewBreadcrumb" class="mb-8">
<h1 class="text-lg text-center font-light relative px-7">
<!-- Cancel -->
<a
href="account-view.html"
<button
@click="$router.go(-1)"
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
><fa icon="chevron-left" class="fa-fw"></fa>
</a>
>
<fa icon="chevron-left" class="fa-fw"></fa>
</button>
[New/Edit] Identity
</h1>
</div>
@@ -17,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>
@@ -35,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>
@@ -49,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

@@ -5,11 +5,11 @@
<div id="ViewBreadcrumb" class="mb-8">
<h1 class="text-lg text-center font-light relative px-7">
<!-- Cancel -->
<a
href="project-view.html"
<router-link
:to="{ name: 'project' }"
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
><fa icon="chevron-left" class="fa-fw"></fa>
</a>
</router-link>
Make Commitment
</h1>

View File

@@ -5,74 +5,281 @@
<div id="ViewBreadcrumb" class="mb-8">
<h1 class="text-lg text-center font-light relative px-7">
<!-- Cancel -->
<a
href="project-view.html"
<router-link
:to="{ name: 'project' }"
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
><fa icon="chevron-left" class="fa-fw"></fa>
</a>
[New/Edit] Project
><fa icon="chevron-left" class="fa-fw"></fa
></router-link>
[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,33 +4,39 @@
<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">
<a href="account-view.html" class="block text-center py-3 px-1"
<router-link
:to="{ name: 'account' }"
class="block text-center py-3 px-1"
><fa icon="circle-user" class="fa-fw"></fa
></a>
></router-link>
</li>
</ul>
</nav>
@@ -41,9 +47,12 @@
<div id="ViewBreadcrumb" class="mb-8">
<h1 class="text-lg text-center font-light relative px-7">
<!-- Back -->
<a href="" class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
><fa icon="chevron-left" class="fa-fw"></fa
></a>
<button
@click="$router.go(-1)"
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
>
<fa icon="chevron-left" class="fa-fw"></fa>
</button>
<!-- Context Menu -->
<a
href=""
@@ -51,42 +60,57 @@
><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 -->
<a
href="commitment-edit.html"
<router-link
:to="{ name: 'new-edit-commitment' }"
class="block text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md mb-8"
>Make Commitment</a
>Make Commitment</router-link
>
<!-- Commitments -->
@@ -121,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>

173
src/views/ProjectsView.vue Normal file
View File

@@ -0,0 +1,173 @@
<template>
<!-- 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>

View File

@@ -12,16 +12,16 @@
<p class="text-center text-xl mb-4 font-light">
Do you already have an identity to import?
</p>
<router-link
:to="{ name: 'new-edit-account' }"
<a
@click="onClickYes()"
class="block w-full text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md mb-2"
>
No
</router-link>
<router-link
:to="{ name: 'import-account' }"
</a>
<a
@click="onClickNo()"
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md"
>Yes</router-link
>Yes</a
>
</div>
</section>
@@ -33,5 +33,13 @@ import { Options, Vue } from "vue-class-component";
@Options({
components: {},
})
export default class StartView extends Vue {}
export default class StartView extends Vue {
public onClickYes() {
this.$router.push({ name: "account" });
}
public onClickNo() {
this.$router.push({ name: "import-account" });
}
}
</script>

View File

@@ -1,4 +1,10 @@
const { defineConfig } = require("@vue/cli-service");
module.exports = defineConfig({
transpileDependencies: true,
configureWebpack: {
devtool: "source-map",
experiments: {
topLevelAwait: true,
},
},
});