Compare commits

..

137 Commits

Author SHA1 Message Date
2a8aa8be78 Merge branch 'master' into kb/add-usage-guide 2024-06-26 13:19:58 -04:00
Kent Bull
23cc923144 docs: finish initial boostrapping dev guide 2024-06-26 11:06:18 -06:00
Kent Bull
38ec7320bb docs: add more docs on local run 2024-06-25 19:30:29 -06:00
Kent Bull
316e4be25a docs: basic pandoc setup 2024-06-25 09:25:58 -06:00
1c0e0aeeba modify & explain icons next to feed 2024-06-25 11:04:40 -04:00
1147ee4707 refactor display logic a bit (no flow changes intended) 2024-06-25 11:04:40 -04:00
e68d4fbe6d passkey test (#116)
Co-authored-by: Trent Larson <trent@trentlarson.com>
Reviewed-on: #116
Co-authored-by: trentlarson <trent@trentlarson.com>
Co-committed-by: trentlarson <trent@trentlarson.com>
2024-06-24 22:21:24 -04:00
c3a1571c2f move & resize the contact edit & info buttons 2024-06-22 12:34:30 -06:00
fafdccae66 bump version and add "-beta" 2024-06-22 12:23:57 -06:00
1611d22892 bump to v 0.3.14 2024-06-22 12:23:10 -06:00
4c6c85983c fix checkbox verbiage when no project is chosen for a give 2024-06-22 12:06:55 -06:00
978a31a34e fix prompt for already-registered contacts (plus some verbiage) 2024-06-22 11:47:10 -06:00
c7c6b7c071 add BX currency, add link for user's activity, tweak verbiage 2024-06-21 20:33:44 -06:00
daf692537c improve messaging when user has no offers or projects 2024-06-21 19:52:35 -06:00
6bc7dfd76d fix justification of checkboxes and text so they don't move 2024-06-21 19:25:46 -06:00
b87142d3ed give-detail page: add more-correct parameters from confirm-give page, and allow toggling of project & user-recipient 2024-06-21 19:13:19 -06:00
15256bf698 tweak UI for give-confirmation screen 2024-06-21 16:02:08 -06:00
886e22ba88 add Confirm Gift screen for simpler confirmation 2024-06-20 20:52:26 -06:00
a85aac9630 fix dependency vulnerabilities 2024-05-24 11:42:36 -06:00
766727c799 bump version and add -beta; enhance help 2024-05-24 10:21:08 -06:00
08b67984e4 bump to verson 0.3.13 2024-05-24 10:19:17 -06:00
50bba70e1f allow link to the large version of a project image 2024-05-24 09:11:20 -06:00
64f5656f41 add an image to projects (which shows on all ProjectIcons except for offers) 2024-05-23 20:51:40 -06:00
e3e2031bd8 bump version and add -beta 2024-05-20 08:27:12 -06:00
3cd164b09d update CHANGELOG 2024-05-19 20:03:30 -06:00
141fb39ad1 bump to version 0.3.12 2024-05-19 19:57:02 -06:00
11b662e326 fix the photo share_target, and tweak other verbiage 2024-05-19 19:56:25 -06:00
2de254f9a1 bump version and add -beta 2024-05-19 19:32:38 -06:00
1bf57d3228 add a global error handler 2024-05-19 16:25:44 -06:00
567bcad88d bump to version 0.3.11 (and enhance warning on profile deletion) 2024-05-19 08:39:18 -06:00
9bdb87e9ef set the correct active camera number when it starts 2024-05-17 20:24:33 -06:00
e7e1176a83 bump version and add -beta 2024-05-17 12:16:23 -06:00
7b6afe25c5 allow any image URL for gifts & profiles 2024-05-12 21:43:18 -06:00
a8ef530d58 allow file choice for gift, plus other UI fixes 2024-05-12 17:55:54 -06:00
ee3d4acb58 fix cropping problem where long images go off the screen 2024-05-12 12:39:16 -06:00
36d2e41fea bump to v 0.3.10, fix image upload on Chrome 2024-05-12 12:12:59 -06:00
03ac31d981 Merge pull request 'add a share_target for people to add a photo' (#115) from share-photo into master
Reviewed-on: #115
2024-05-11 20:03:33 -04:00
b81c096fe4 add file-chooser to the profile image selection 2024-05-11 12:30:10 -06:00
6bcc0023cd style the sharing screen (plus other fixes) 2024-05-11 07:09:48 -06:00
aa7d82c531 add a share_target for people to add a photo 2024-05-10 13:17:20 -06:00
a95c398e81 increment version and add "-beta" 2024-04-28 20:10:39 -06:00
874e717e69 bump to version 0.3.9 2024-04-28 20:09:56 -06:00
c107073592 disallow new-project page if not registered 2024-04-28 19:16:29 -06:00
3ea5c42769 remove verbiage on front page that's now extra 2024-04-28 19:04:44 -06:00
9157837586 show something to indicate claims were sent (mostly in BVC screens) 2024-04-28 18:36:06 -06:00
751c066bd0 constantly recheck on home screen if not registered 2024-04-28 17:02:31 -06:00
c403356055 add registration inside contact import, with flag to hide it 2024-04-28 16:18:30 -06:00
009a7ecdf8 add 'registered' flag in contact info 2024-04-28 13:12:26 -06:00
bd148e88a3 for scan on QR code screen, import and keep on that screen 2024-04-27 20:33:10 -06:00
bca5adecc9 add tweaks to testing instructions 2024-04-27 14:59:23 -06:00
73f9d7f9e9 add page to view all claims about a DID (which we'll have to restrict to visible people soon) 2024-04-26 20:13:44 -06:00
6f4876e32b fix problem with duplicates in feed, plus some other UI tweaks 2024-04-26 17:05:11 -06:00
c1fe8216f6 allow loading more gives & offers & plans when limits are hit on project view 2024-04-26 15:44:09 -06:00
56523da11e remove some 'uppercase' CSS markers 2024-04-25 20:17:49 -06:00
35ec7fd43c put button directly on contacts page to show the given totals 2024-04-24 20:38:34 -06:00
94b5389ce9 change remainder of "confirm" calls to better UX 2024-04-24 20:11:38 -06:00
421b4c1719 replace many of the javascript "confirm" calls with the nicer UX version 2024-04-24 19:52:33 -06:00
6ce3a0703c remove 'moment' library that's no longer used 2024-04-24 18:56:09 -06:00
79b14355d9 add choice of a start date for a project 2024-04-23 20:48:38 -06:00
c5102f89b5 add note about confirming your own, plus other helpful verbiage, plus notify messages that don't linger 2024-04-23 09:13:57 -06:00
ab6d2e3d4b remove message confusion, add project name during give-details 2024-04-21 20:31:57 -06:00
d1a285d659 change the "give" action on contact page to use dialog box 2024-04-21 16:42:22 -06:00
bba183dc46 add 'offer' on contact screen 2024-04-21 07:38:59 -06:00
676882978a add code to display profiles in feed, but deactivate it for now 2024-04-20 19:53:11 -06:00
dc9720560e increment version and add "-beta" 2024-04-20 08:14:53 -06:00
15c026c80c bump to v 0.3.8 2024-04-20 08:06:34 -06:00
03f722f38a make so cropping isn't behind header; delete profile image from storage when deleted 2024-04-19 20:13:44 -06:00
f14c3de0ef Merge pull request 'profile-pic' (#114) from profile-pic into master
Reviewed-on: #114
2024-04-19 17:36:53 -04:00
606f21faec make the home screen elements load more quickly 2024-04-19 15:37:10 -06:00
5a9958cb4f show contact's or user's icon in more places 2024-04-19 11:39:01 -06:00
b11cf81bf9 crop the image and store online and in settings 2024-04-18 20:27:43 -06:00
734e28667d add photo to profile page (not yet saved) 2024-04-17 20:07:09 -06:00
405bc22dae fix contact sorting to show those without names 2024-04-17 19:29:17 -06:00
1758cbee98 update ClickUp link to a public link 2024-04-17 11:05:34 -06:00
5359b241f7 remove tasks here in favor of ClickUp 2024-04-16 20:13:04 -06:00
555ac34d18 note that tasks have moved 2024-04-11 20:43:52 -06:00
acbbdf0e8b bump version and add "-beta" 2024-04-10 19:40:16 -06:00
cf18f1543a bump to v 0.3.7 2024-04-10 19:32:46 -06:00
df829778da open the app when notification is clicked 2024-04-10 19:31:14 -06:00
7ae431a9e7 fix PWA creation & service-worker registration, plus some commentary tweaks 2024-04-09 20:29:21 -06:00
77becf8673 remove non-working interests, enhance error messages, update tasks & changelog 2024-04-09 17:54:17 -06:00
a0ef8b6fd3 Merge pull request 'vitejs refactor' (#110) from jsnbuchanan/crowd-funder-for-time-pwa:feat/vitejs into master
Reviewed-on: #110
2024-04-09 19:49:48 -04:00
1befff0abd Merge pull request 'misc tweaks for new vite build' (#4) from trentlarson/crowd-funder-from-jason:feat/vitejs-trent3 into feat/vitejs
Reviewed-on: jsnbuchanan/crowd-funder-for-time-pwa#4
2024-04-09 06:05:55 -04:00
0b446ec134 misc tweaks for new vite build 2024-04-07 18:12:33 -06:00
333ac773f6 Merge pull request 'A couple small fixes, plus a merge from master' (#1) from trentlarson/crowd-funder-from-jason:feat/vitejs-trent into feat/vitejs
Reviewed-on: jsnbuchanan/crowd-funder-for-time-pwa#1
2024-04-07 13:52:43 -04:00
1459719a47 remove a lingering debug console.log 2024-04-07 11:39:13 -06:00
5994365a6c fix title of the test app 2024-04-07 11:32:53 -06:00
28f72640d7 add linting before any build 2024-04-07 11:22:20 -06:00
ab81648aca fix linting 2024-04-07 11:02:54 -06:00
5828a290c7 Merge remote-tracking branch 'original-origin/master' into feat/vitejs-trent 2024-04-07 09:41:14 -06:00
78fab735e6 avoid a huge error message in a likely-well-known scenario 2024-04-07 09:24:55 -06:00
2ae165d56f reorder home page vapid check to avoid an error on localhost 2024-04-07 09:16:42 -06:00
0fbd1ad51a add missing Dexie import (which causes failure upon download click) 2024-04-07 09:13:32 -06:00
d49bf61524 on home page, change the filtered button color 2024-04-06 17:58:10 -06:00
1a80bbb714 Merge pull request 'ui-additions-2024-03' (#113) from ui-additions-2024-03 into master
Reviewed-on: #113
2024-04-06 19:46:16 -04:00
5a2a8659f7 Merge branch 'master' into ui-additions-2024-03 2024-04-06 17:45:32 -06:00
0632fb9b39 show in description when recipient is a project (not just Anonymous) 2024-04-06 17:39:40 -06:00
640d273646 filter by selections (now all working), add cache for plans 2024-04-06 14:01:18 -06:00
8f4289c14d Merge pull request 'send a time for notifications to the push server' (#112) from notify-time into master
Reviewed-on: #112
2024-04-03 22:04:36 -04:00
7f56c90d97 Merge pull request 'ui-fixes-2024-03' (#111) from ui-fixes-2024-03 into master
Reviewed-on: #111
2024-04-03 22:04:02 -04:00
8e1daf7015 feed filter: save the changed values to the DB, go to map if no location chosen, reload if necessary 2024-04-03 19:54:01 -06:00
Jose Olarte III
e90a0be6d9 Names and variables for filter toggles 2024-04-03 15:51:23 +08:00
489bb76a60 adjust more code to the PushSubscriptionJSON 2024-04-02 19:32:41 -06:00
2ca33bb9eb adjust the notification-subscription objects to try and send correct info 2024-04-02 19:18:31 -06:00
121181c6a1 add adjustment to UTC hour for notification time 2024-04-02 18:38:44 -06:00
e4cf79b558 update tasks 2024-04-01 19:06:18 -06:00
7692cc2b35 add logic to send a time for notifications 2024-04-01 19:04:54 -06:00
Jose Olarte III
0b4f2484f7 Additions to Account View 2024-03-29 21:41:14 +08:00
Jose Olarte III
cfd53bc186 Removed one more 2024-03-29 15:55:16 +08:00
Jose Olarte III
7f66addfe3 Filter options reduced for release 2024-03-29 15:53:46 +08:00
Jose Olarte III
4635c1ac48 Feed filters dialog 2024-03-27 19:57:31 +08:00
Jose Olarte III
55da3d0b1c Map fix #2 2024-03-26 21:38:21 +08:00
Jose Olarte III
dce4d3cc72 Button width changes
For buttons that are next to each other
2024-03-26 19:55:16 +08:00
Jose Olarte III
e028197a2a Optimized grid space for wider screens 2024-03-26 17:12:55 +08:00
Jose Olarte III
0dab475d8b Fixed map z-index 2024-03-26 16:54:43 +08:00
Jose Olarte III
4e227fc07a Added close icon to gifted prompts dialog 2024-03-26 16:02:24 +08:00
2dfc8fedaa refactor tasks 2024-03-25 19:03:01 -06:00
035f2a5b04 docs: adding do for updated development server run command
- `npm run dev`
2024-03-25 08:15:04 -06:00
09dccc34d6 fix: buffer typescript error in util.ts when parsing ArrayBuffer 2024-03-25 08:10:38 -06:00
b28104af5b bump version and add -beta 2024-03-24 19:04:24 -06:00
3a07e31d63 bump to version 0.3.6 2024-03-24 18:28:42 -06:00
35455e6648 fix check for more camera-device options 2024-03-24 18:27:06 -06:00
0e2c5af16e add onboarding help instructions as separate page 2024-03-24 17:01:53 -06:00
1fc5b0ea2b Merge pull request 'add button during photo to switch to mirror mode' (#109) from photo-reverse into master
Reviewed-on: #109
2024-03-24 18:59:50 -04:00
ca240ab795 fix: es modules syntax for buffer deps instead of commonjs require 2024-03-24 13:05:22 -06:00
01b5ca6ec8 chore: update vitejs config to deploy on the same default port as the @vue/cli-service
This port is 8080. This is done to not break existing tooling and devops code.
2024-03-24 12:05:09 -06:00
6f49260c1e fix: AccountViewView.vue template not resolving dep for dexie-export-import/dist/import
Previous error:

Error: The following dependencies are imported but could not be resolved:

  dexie-export-import/dist/import (imported by /Users/jason/dev/src/trent/crowd-funder-for-time-pwa/src/views/AccountViewView.vue?id=0)

Are they installed?
    at file:///Users/jason/dev/src/trent/crowd-funder-for-time-pwa/node_modules/vite/dist/node/chunks/dep-DJaaTb_D.js:52506:23
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
    at async file:///Users/jason/dev/src/trent/crowd-funder-for-time-pwa/node_modules/vite/dist/node/chunks/dep-DJaaTb_D.js:51972:38
2024-03-24 11:51:58 -06:00
38f44771e9 Initial stab at vitejs update 2024-03-24 11:18:29 -06:00
be2154f847 change icon for detail view (from circle-info to file-lines) 2024-03-23 19:07:53 -06:00
4ad4cab25e add blurb explaining what data is shared with the world 2024-03-23 18:45:26 -06:00
e020caaa50 show warnings before dismissing prompt, and add to tasks and help 2024-03-23 17:35:58 -06:00
40d12b1f9c add button on photo to switch to mirror mode 2024-03-23 16:31:23 -06:00
28754bdfb1 bump version to 0.3.5 2024-03-23 02:41:25 -06:00
2b8f9579f1 fix so that project agent & location removals get saved 2024-03-23 02:31:44 -06:00
6dc0c2cd58 add a camera-switch button 2024-03-23 01:32:55 -06:00
cc6d0958dc Merge pull request 'fix: npm audit fix to resolve vulnerabilities 1 low, 3 moderate, 1 high' (#108) from jsnbuchanan/crowd-funder-for-time-pwa:master into master
Reviewed-on: #108
2024-03-22 12:01:21 -04:00
7ce00b86e8 deps: npm audit fix to resolve vulnerabilities 1 low, 3 moderate, 1 high
There are still 9 moderate severity vulnerabilities, but I will work on those independentally because they may involve updating to library version that have breaking changes.
2024-03-22 09:29:42 -06:00
60 changed files with 1913 additions and 1757 deletions

View File

@@ -47,7 +47,7 @@ npm run lint
``` ```
# (Let's replace this with a .env.development or .env.staging file.) # (Let's replace this with a .env.development or .env.staging file.)
# The test BVC_MEETUPS_PROJECT_CLAIM_ID does not resolve as a URL because it's only in the test DB and the prod redirect won't redirect there. # The test BVC_MEETUPS_PROJECT_CLAIM_ID does not resolve as a URL because it's only in the test DB and the prod redirect won't redirect there.
TIME_SAFARI_APP_TITLE="TimeSafari_Test" VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HNTZYJJXTGT0EZS3VEJGX7AK VITE_DEFAULT_ENDORSER_API_SERVER=https://test-api.endorser.ch VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app PASSKEYS_ENABLED=yep npm run build TIME_SAFARI_APP_TITLE="TimeSafari_Test" VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HNTZYJJXTGT0EZS3VEJGX7AK VITE_DEFAULT_ENDORSER_API_SERVER=https://test-api.endorser.ch VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app npm run build
``` ```
* Production * Production

66
docs/README.md Normal file
View File

@@ -0,0 +1,66 @@
# TimeSafari Docs
## Generating PDF from Markdown on OSx
This uses Pandoc and BasicTex (LaTeX) Installed through Homebrew.
### Set Up
```bash
# See https://daniel.feldroy.com/posts/setting-up-latex-on-mac-os-x
brew install pandoc
brew install basictex
pandoc keystore-migration.md -o keystore-migration.pdf
# Setting up LaTex packages
# First update tlmgr
sudo tlmgr update --self
# Then install LaTex packages
sudo tlmgr install titlesec
sudo tlmgr install framed
sudo tlmgr install threeparttable
sudo tlmgr install wrapfig
sudo tlmgr install multirow
sudo tlmgr install enumitem
sudo tlmgr install bbding
sudo tlmgr install titling # Required for the fancy headers used
sudo tlmgr install tabu
sudo tlmgr install mdframed
sudo tlmgr install tcolorbox
sudo tlmgr install textpos
sudo tlmgr install import
sudo tlmgr install varwidth
sudo tlmgr install needspace
sudo tlmgr install tocloft # Required for \tableofcontents generation
sudo tlmgr install ntheorem
sudo tlmgr install environ
sudo tlmgr install trimspaces
sudo tlmgr install lastpage # Enables Page X of Y
sudo tlmgr install collection-fontsrecommended # And set up fonts
sudo tlmgr install libertine # The main font the doc uses
```
### Usage
Use the `pandoc` command to generate a PDF.
```bash
pandoc usage-guide.md -o usage-guide.pdf
```
And you can open the PDF with the `open` command.
```bash
open usage-guide.pdf
```
Or use this one-liner
```bash
pandoc usage-guide.md -o usage-guide.pdf && open usage-guide.pdf
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 463 KiB

316
docs/usage-guide.md Normal file
View File

@@ -0,0 +1,316 @@
---
geometry: margin=1in
header-includes:
- \usepackage{graphicx}
- \usepackage{titling}
- \usepackage{fancyhdr}
- \usepackage{lastpage}
- \pagestyle{fancy}
- \fancyhead[L]{Time Safari Usage Guide}
- \fancyhead[C]{Page \thepage\ of \pageref{LastPage}}
- \fancyhead[R]{}
- \fancyfoot[L]{}
- \fancyfoot[C]{}
- \fancyfoot[R]{\includegraphics[width=1cm]{images/timesafari-logo-binoculars.png}}
- \usepackage{tocloft}
- \usepackage{libertine}
- \renewcommand{\familydefault}{\sfdefault}
- \fancypagestyle{tocstyle}{
\fancyhead[L]{Time Safari Usage Guide}
\fancyhead[C]{Page \thepage\ of \pageref{LastPage}}
\fancyhead[R]{}
\fancyfoot[L]{}
\fancyfoot[C]{}
\fancyfoot[R]{\includegraphics[width=1cm]{images/timesafari-logo-binoculars.png}}}
---
\begin{titlepage}
\centering
\vspace*{\fill}
{\huge\textbf{TimeSafari Usage guide}}
\vspace{1cm}
{\Large Signing up users, adding contacts, and adding gifts.}
\vspace{1cm}
\includegraphics[width=0.5\textwidth]{images/timesafari-logo.png}
\vspace*{\fill}
\vspace{1cm}
{\Large Trent Larson, Kent Bull}
\vspace{0.5cm}
{\large 2024-06-25}
\end{titlepage}
\clearpage
\begin{center}
\includegraphics[width=2cm]{images/timesafari-logo-binoculars.png}
\end{center}
\tableofcontents
\clearpage
# Purpose of Document
Both end-users and development team members need to know how to use TimeSafari.
This document serves to show how to use every feature of the TimeSafari platform.
Sections of this document are geared specifically for software developers and quality assurance
team members.
Companion videos will also describe end-to-end workflows for the end-user.
# TimeSafari
## Overview
\pagebreak
# 1 - End Users
This section covers application usage for people who will use TimeSafari as intended. It is a
simplified guide illustrating how to gain value from using TimeSafari.
\pagebreak
# 2 - Software Developers
This section is tailored for software developers seeking to use the application during development,
quality assurance, and testing.
# Bootstrapping a local development environment
The first concern a software developer has when working on TimeSafari is to set up a local
development environment. This section will guide you through the process.
## Prerequisites
1. Have the following installed on your local machine:
- Node.js and NPM
- A web browser. For this guide, we will use Google Chrome.
- Git
- A code editor
2. Create an API key on Infura. This is necessary for the Endorser API to connect to the Ethereum
blockchain.
- You can create an account on Infura [here](https://infura.io/).\
Click "CREATE NEW API KEY" and label the key. Then click "API Keys" in the top menu bar to
be taken back to the list of keys.
Click "VIEW STATS" on the key you want to use.
![](images/01_infura-api-keys.png){ width=550px }
- Go to the key detail page. Then click "MANAGE API KEY".
![](images/02-infura-key-detail.png){ width=550px }
- Click the copy and paste button next to the string of alphanumeric characters.\
This is your API, also known as your project ID.
![](images/03-infura-api-key-id.png){width=550px }
- Save this for later during the Endorser API setup. This will go in your `INFURA_PROJECT_ID`
environment variable.
## Setup steps
### 1. Clone the following repositories from their respective Git hosts:
- [TimeSafari Frontend](https://gitea.anomalistdesign.com/trent_larson/crowd-funder-for-time-pwa)\
This is a Progressive Web App (PWA) built with VueJS and TypeScript.
Note that the clone command here is different from the one you would use for GitHub.
```bash
git clone git clone \
ssh://git@gitea.anomalistdesign.com:222/trent_larson/crowd-funder-for-time-pwa.git
```
- [TimeSafari Backend - Endorser API](https://github.com/trentlarson/endorser-ch)\
This is a NodeJS service providing the backend for TimeSafari.
```bash
git clone git@github.com:trentlarson/endorser-ch.git
```
\pagebreak
### 2. Database creation
#### Alternative 1 - use test data
To generate a development database and perform user setup you can run a local test with instructions
below to generate sample data. Then copy the test database, rename it to `-dev` as below:\
`cp ../endorser-ch-test-local.sqlite3 ../endorser-ch-dev.sqlite3` \
and rerun `npm run dev` to give yourself user #0 and others from the ETHR_CRED_DATA in [the endorser.ch test util file](https://github.com/trentlarson/endorser-ch/blob/master/test/util.js#L90)
#### Alternative 2 - boostrap single seed user
In this method you will end up with two accounts in the database, one for the first boostrap user,
and the second as the primary user you will use during testing. The first user will invite the
second user to the app.
1. Install dependencies and environment variables.\
In endorser-ch install dependencies and set up environment variables to allow starting it up in
development mode.
```bash
cd endorser-ch
npm clean install # or npm ci
cp .env.local .env
```
Edit the .env file's INFURA_PROJECT_ID with the value you saved earlier in the
prerequisites.\
Then create the SQLite database by running `npm run flyway migrate` with environment variables
set correctly to select the default SQLite development user as follows.
```bash
export NODE_ENV=dev
export DBUSER=sa
export DBPASS=sasa
npm run flyway migrate
```
The first run of flyway migrate may take some time to complete because the entire Flyway
distribution must be downloaded prior to executing migrations.
Successful output looks similar to the following:
```
Database: jdbc:sqlite:../endorser-ch-dev.sqlite3 (SQLite 3.41)
Schema history table "main"."flyway_schema_history" does not exist yet
Successfully validated 10 migrations (execution time 00:00.034s)
Creating Schema History table "main"."flyway_schema_history" ...
Current version of schema "main": << Empty Schema >>
Migrating schema "main" to version "1 - initial-anew"
Migrating schema "main" to version "2 - registration"
Migrating schema "main" to version "3 - plan project"
Migrating schema "main" to version "4 - offer gave"
Migrating schema "main" to version "5 - more confirmations"
Migrating schema "main" to version "6 - providers urls"
Migrating schema "main" to version "7 - hash nonce"
Migrating schema "main" to version "8 - project location"
Migrating schema "main" to version "9 - plan links"
Migrating schema "main" to version "10 - gift or trade"
Successfully applied 10 migrations to schema "main", now at version v10 (execution time 00:00.043s)
A Flyway report has been generated here: /Users/kbull/code/timesafari/endorser-ch/report.html
```
\pagebreak
2. Generate the first user in TimeSafari PWA and bootstrap that user in Endorser's database.\
As TimeSafari is an invite-only platform the first user must be manually bootstrapped since
no other users exist to be able to invite the first user. This first user must be added manually
to the SQLite database used by Endorser. In this setup you generate the first user from the PWA.
This user is automatically generated on first usage of the TimeSafari PWA. Bootstrapping that
user is required so that this first user can register other users.
- Change directories into `crowd-funder-for-time-pwa`
```bash
cd ..
cd crowd-funder-for-time-pwa
```
- Ensure the `.env.development` file exists and has the following values:
```env
VITE_DEFAULT_ENDORSER_API_SERVER=http://127.0.0.1:3000
```
- Install dependencies and run in dev mode. For now don't worry about configuring the app. All we
need is to generate the first root user and this happens automatically on app startup.
```bash
npm clean install # or npm ci
npm run dev
```
- Open the app in a browser and go to the developer tools. It is recommended to use a completely
separate browser profile so you do not clear out your existing user account. We will be
completely resetting the PWA app state prior to generating the first user.
In the Developer Tools go to the Application tab.
![](images/04-pwa-chrome-devtools.png){width=350px}
Click the "Clear site data" button and then refresh the page.
- Click the account button in the bottom right corner of the page.
![](images/05-pwa-account-button.png){width=150px}
- This will take you to the account page titled "Your Identity" on which you can see your DID,
a `did:ethr` DID in this case.
![](images/06-pwa-account-page.png){width=350px}
- Copy the DID by selecting it and copying it to the clipboard or by clicking the copy and paste
button as shown in the image.
![](images/07-pwa-did-copied.png){width=200px}
In our case this DID is:\
`did:ethr:0xe4B783c74c8B0e229524e44d0cD898D272E02CD6`
- Add that DID to the following echoed SQL statement where it says `YOUR_DID`
```bash
echo "INSERT INTO registration (did, maxClaims, maxRegs, epoch)
VALUES ('YOUR_DID', 100, 10000, 1719348718092);"
| sqlite3 ./endorser-ch-dev.sqlite3
```
and run this command in the parent directory just above the `endorser-ch` directory.
It needs to be the parent directory of your `endorser-ch` repository because when
`endorser-ch` creates the SQLite database it depends on it creates it in the parent directory
of `endorser-ch`.
- You can verify with an SQL browser tool that your record has been added to the `registration`
table.
![](images/08-endorser-sqlite-row-added.png){width=350px}
3. Then start the Endorser service in development mode with the following commands.
```bash
cd ./endorser-ch
export NODE_ENV=dev
npm run dev
```
This starts the Endorser service on port 3000.
4. Create the second user by opening up a separate browser profile or incognito session, opening the
TimeSafari PWA at `http://localhost:8080`. You will see the yellow banner stating "Someone must
register you before you can give or offer."
![](images/09-pwa-second-profile-first-open.png){width=350px}
- If you want to ensure you have a fresh user account then open the developer tools, clear the
Application data as before, and then refresh the page. This will generate a new user in the
browser's IndexedDB database.
5. Go to the second users' account page to copy the DID.
![](images/10-pwa-second-user-did.png){width=350px}
6. Copy the DID and put it in the text bar on the "Your Contacts" page for the first account
![](images/11-pwa-first-user-add-contact.png){width=350px}
7. Click the "+" plus icon to add the user.
![](images/12-pwa-first-user-contact-added.png){width=350px}
8. Then click the register button to register the second user.
![](images/13-pwa-first-user-register-second-user-btn.png){width=350px}
9. Click "YES" on the dialog that shows up.
![](images/14-pwa-first-user-register-yes.png){width=350px}
After this a notification will pop up indicating whether registration was successful or not.
10. You have finished the initial set up of users.

View File

@@ -181,7 +181,6 @@
class="block w-full text-center text-md font-bold uppercase bg-blue-600 text-white px-2 py-2 rounded-md mb-2" class="block w-full text-center text-md font-bold uppercase bg-blue-600 text-white px-2 py-2 rounded-md mb-2"
> >
Yes Yes
{{ notification.yesText ? ", " + notification.yesText : "" }}
</button> </button>
<button <button
@@ -193,7 +192,7 @@
" "
class="block w-full text-center text-md font-bold uppercase bg-yellow-600 text-white px-2 py-2 rounded-md mb-2" class="block w-full text-center text-md font-bold uppercase bg-yellow-600 text-white px-2 py-2 rounded-md mb-2"
> >
No {{ notification.noText ? ", " + notification.noText : "" }} No
</button> </button>
<label <label

View File

@@ -287,10 +287,11 @@ export default class GiftedDialog extends Vue {
unitCode: string = "HUR", unitCode: string = "HUR",
) { ) {
try { try {
const identity = await libsUtil.getIdentity(this.activeDid);
const result = await createAndSubmitGive( const result = await createAndSubmitGive(
this.axios, this.axios,
this.apiServer, this.apiServer,
this.activeDid, identity,
giverDid, giverDid,
this.receiver?.did as string, this.receiver?.did as string,
description, description,

View File

@@ -223,10 +223,11 @@ export default class OfferDialog extends Vue {
} }
try { try {
const identity = await libsUtil.getIdentity(this.activeDid);
const result = await createAndSubmitOffer( const result = await createAndSubmitOffer(
this.axios, this.axios,
this.apiServer, this.apiServer,
this.activeDid, identity,
description, description,
amount, amount,
unitCode, unitCode,

View File

@@ -126,6 +126,7 @@ import { Component, Vue } from "vue-facing-decorator";
import VuePictureCropper, { cropper } from "vue-picture-cropper"; import VuePictureCropper, { cropper } from "vue-picture-cropper";
import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "@/constants/app"; import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "@/constants/app";
import { getIdentity } from "@/libs/util";
import { db } from "@/db/index"; import { db } from "@/db/index";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto"; import { accessToken } from "@/libs/crypto";
@@ -347,7 +348,8 @@ export default class PhotoDialog extends Vue {
this.blob = (await cropper?.getBlob()) || undefined; this.blob = (await cropper?.getBlob()) || undefined;
} }
const token = await accessToken(this.activeDid); const identifier = await getIdentity(this.activeDid);
const token = await accessToken(identifier);
const headers = { const headers = {
Authorization: "Bearer " + token, Authorization: "Bearer " + token,
}; };

View File

@@ -1,11 +1,12 @@
import axios from "axios"; import axios from "axios";
import * as R from "ramda";
import * as THREE from "three"; import * as THREE from "three";
import { GLTFLoader } from "three/addons/loaders/GLTFLoader"; import { GLTFLoader } from "three/addons/loaders/GLTFLoader";
import * as SkeletonUtils from "three/addons/utils/SkeletonUtils"; import * as SkeletonUtils from "three/addons/utils/SkeletonUtils";
import * as TWEEN from "@tweenjs/tween.js"; import * as TWEEN from "@tweenjs/tween.js";
import { db } from "@/db"; import { accountsDB, db } from "@/db";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { getHeaders } from "@/libs/endorserServer"; import { accessToken } from "@/libs/crypto";
const ANIMATION_DURATION_SECS = 10; const ANIMATION_DURATION_SECS = 10;
const ENDORSER_ENTITY_PREFIX = "https://endorser.ch/entity/"; const ENDORSER_ENTITY_PREFIX = "https://endorser.ch/entity/";
@@ -18,7 +19,17 @@ export async function loadLandmarks(vue, world, scene, loop) {
const settings = await db.settings.get(MASTER_SETTINGS_KEY); const settings = await db.settings.get(MASTER_SETTINGS_KEY);
const activeDid = settings?.activeDid || ""; const activeDid = settings?.activeDid || "";
const apiServer = settings?.apiServer; const apiServer = settings?.apiServer;
const headers = await getHeaders(activeDid); await accountsDB.open();
const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === activeDid, accounts);
const headers = {
"Content-Type": "application/json",
};
const identity = JSON.parse(account?.identity || "null");
if (identity) {
const token = await accessToken(identity);
headers["Authorization"] = "Bearer " + token;
}
const url = apiServer + "/api/v2/report/claims?claimType=GiveAction"; const url = apiServer + "/api/v2/report/claims?claimType=GiveAction";
const resp = await axios.get(url, { headers: headers }); const resp = await axios.get(url, { headers: headers });

View File

@@ -4,10 +4,6 @@
* See also ../libs/veramo/setup.ts * See also ../libs/veramo/setup.ts
*/ */
export enum AppString { export enum AppString {
// This is used in titles and verbiage inside the app.
// There is also an app name without spaces, for packaging in the package.json file used in the manifest.
APP_NAME = "Time Safari",
PROD_ENDORSER_API_SERVER = "https://api.endorser.ch", PROD_ENDORSER_API_SERVER = "https://api.endorser.ch",
TEST_ENDORSER_API_SERVER = "https://test-api.endorser.ch", TEST_ENDORSER_API_SERVER = "https://test-api.endorser.ch",
LOCAL_ENDORSER_API_SERVER = "http://localhost:3000", LOCAL_ENDORSER_API_SERVER = "http://localhost:3000",
@@ -36,9 +32,6 @@ export const DEFAULT_PUSH_SERVER =
export const IMAGE_TYPE_PROFILE = "profile"; export const IMAGE_TYPE_PROFILE = "profile";
export const PASSKEYS_ENABLED =
!!import.meta.env.VITE_PASSKEYS_ENABLED || false;
/** /**
* The possible values for "group" and "type" are in App.vue. * The possible values for "group" and "type" are in App.vue.
* From the notiwind package * From the notiwind package
@@ -48,10 +41,8 @@ export interface NotificationIface {
type: string; // "toast" | "info" | "success" | "warning" | "danger" type: string; // "toast" | "info" | "success" | "warning" | "danger"
title: string; title: string;
text?: string; text?: string;
noText?: string;
onCancel?: (stopAsking: boolean) => Promise<void>; onCancel?: (stopAsking: boolean) => Promise<void>;
onNo?: (stopAsking: boolean) => Promise<void>; onNo?: (stopAsking: boolean) => Promise<void>;
onYes?: () => Promise<void>; onYes?: () => Promise<void>;
promptToStopAsking?: boolean; promptToStopAsking?: boolean;
yesText?: string;
} }

View File

@@ -26,7 +26,6 @@ export type Settings = {
lastName?: string; // deprecated - put all names in firstName lastName?: string; // deprecated - put all names in firstName
lastNotifiedClaimId?: string; lastNotifiedClaimId?: string;
lastViewedClaimId?: string; lastViewedClaimId?: string;
passkeyExpirationMinutes?: number; // passkey access token time-to-live in minutes
profileImageUrl?: string; profileImageUrl?: string;
reminderTime?: number; // Time in milliseconds since UNIX epoch for reminders reminderTime?: number; // Time in milliseconds since UNIX epoch for reminders
reminderOn?: boolean; // Toggle to enable or disable reminders reminderOn?: boolean; // Toggle to enable or disable reminders
@@ -38,7 +37,6 @@ export type Settings = {
}>; }>;
showContactGivesInline?: boolean; // Display contact inline or not showContactGivesInline?: boolean; // Display contact inline or not
showGeneralAdvanced?: boolean; // Show advanced features which don't have their own flag
showShortcutBvc?: boolean; // Show shortcut for Bountiful Voluntaryist Community actions showShortcutBvc?: boolean; // Show shortcut for Bountiful Voluntaryist Community actions
vapid?: string; // VAPID (Voluntary Application Server Identification) field for web push vapid?: string; // VAPID (Voluntary Application Server Identification) field for web push
warnIfProdServer?: boolean; // Warn if using a production server warnIfProdServer?: boolean; // Warn if using a production server
@@ -47,7 +45,7 @@ export type Settings = {
}; };
export function isAnyFeedFilterOn(settings: Settings): boolean { export function isAnyFeedFilterOn(settings: Settings): boolean {
return !!(settings?.filterFeedByNearby || settings?.filterFeedByVisible); return !!(settings.filterFeedByNearby || settings.filterFeedByVisible);
} }
/** /**
@@ -61,5 +59,3 @@ export const SettingsSchema = {
* Constants. * Constants.
*/ */
export const MASTER_SETTINGS_KEY = 1; export const MASTER_SETTINGS_KEY = 1;
export const DEFAULT_PASSKEY_EXPIRATION_MINUTES = 15;

View File

@@ -3,13 +3,11 @@ import { getRandomBytesSync } from "ethereum-cryptography/random";
import { entropyToMnemonic } from "ethereum-cryptography/bip39"; import { entropyToMnemonic } from "ethereum-cryptography/bip39";
import { wordlist } from "ethereum-cryptography/bip39/wordlists/english"; import { wordlist } from "ethereum-cryptography/bip39/wordlists/english";
import { HDNode } from "@ethersproject/hdnode"; import { HDNode } from "@ethersproject/hdnode";
import * as didJwt from "did-jwt";
import * as u8a from "uint8arrays";
import { import { ENDORSER_JWT_URL_LOCATION } from "@/libs/endorserServer";
createEndorserJwtForDid,
ENDORSER_JWT_URL_LOCATION,
} from "@/libs/endorserServer";
import { DEFAULT_DID_PROVIDER_NAME } from "../veramo/setup"; import { DEFAULT_DID_PROVIDER_NAME } from "../veramo/setup";
import { decodeEndorserJwt } from "@/libs/crypto/vc";
export const DEFAULT_ROOT_DERIVATION_PATH = "m/84737769'/0'/0'/0'"; export const DEFAULT_ROOT_DERIVATION_PATH = "m/84737769'/0'/0'/0'";
@@ -85,21 +83,82 @@ export const generateSeed = (): string => {
}; };
/** /**
* Retrieve an access token, or "" if no DID is provided. * Retreive an access token
* *
* @param {IIdentifier} identifier
* @return {*} * @return {*}
*/ */
export const accessToken = async (did?: string) => { export const accessToken = async (identifier: IIdentifier) => {
if (did) { 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 nowEpoch = Math.floor(Date.now() / 1000);
const endEpoch = nowEpoch + 60; // add one minute const endEpoch = nowEpoch + 60; // add one minute
const tokenPayload = { exp: endEpoch, iat: nowEpoch, iss: did }; const tokenPayload = { exp: endEpoch, iat: nowEpoch, iss: did };
return createEndorserJwtForDid(did, tokenPayload); const alg = undefined; // defaults to 'ES256K', more standardized but harder to verify vs ES256K-R
} else { const jwt: string = await didJwt.createJWT(tokenPayload, {
return ""; 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(import.meta.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");
}
/** /**
@return results of uportJwtPayload: @return results of uportJwtPayload:
{ iat: number, iss: string (DID), own: { name, publicEncKey (base64-encoded key) } } { iat: number, iss: string (DID), own: { name, publicEncKey (base64-encoded key) } }
@@ -116,7 +175,7 @@ export const getContactPayloadFromJwtUrl = (jwtUrlText: string) => {
} }
// JWT format: { header, payload, signature, data } // JWT format: { header, payload, signature, data }
const jwt = decodeEndorserJwt(jwtText); const jwt = didJwt.decodeJWT(jwtText);
return jwt.payload; return jwt.payload;
}; };

View File

@@ -55,8 +55,8 @@ export function isoUint8ArrayConcat(arrays: Uint8Array[]): Uint8Array {
} }
// from https://github.com/MasterKale/SimpleWebAuthn/blob/master/packages/server/src/helpers/iso/isoCrypto/getWebCrypto.ts // from https://github.com/MasterKale/SimpleWebAuthn/blob/master/packages/server/src/helpers/iso/isoCrypto/getWebCrypto.ts
let webCrypto: { subtle: SubtleCrypto } | undefined = undefined; let webCrypto: unknown = undefined;
export function getWebCrypto(): Promise<{ subtle: SubtleCrypto }> { export function getWebCrypto() {
/** /**
* Hello there! If you came here wondering why this method is asynchronous when use of * Hello there! If you came here wondering why this method is asynchronous when use of
* `globalThis.crypto` is not, it's to minimize a bunch of refactor related to making this * `globalThis.crypto` is not, it's to minimize a bunch of refactor related to making this
@@ -67,8 +67,7 @@ export function getWebCrypto(): Promise<{ subtle: SubtleCrypto }> {
* TODO: If it's after February 2025 when you read this then consider whether it still makes sense * TODO: If it's after February 2025 when you read this then consider whether it still makes sense
* to keep this method asynchronous. * to keep this method asynchronous.
*/ */
const toResolve: Promise<{ subtle: SubtleCrypto }> = new Promise( const toResolve = new Promise((resolve, reject) => {
(resolve, reject) => {
if (webCrypto) { if (webCrypto) {
return resolve(webCrypto); return resolve(webCrypto);
} }
@@ -76,19 +75,17 @@ export function getWebCrypto(): Promise<{ subtle: SubtleCrypto }> {
* Naively attempt to access Crypto as a global object, which popular ESM-centric run-times * Naively attempt to access Crypto as a global object, which popular ESM-centric run-times
* support (and Node v20+) * support (and Node v20+)
*/ */
const _globalThisCrypto = const _globalThisCrypto = _getWebCryptoInternals.stubThisGlobalThisCrypto();
_getWebCryptoInternals.stubThisGlobalThisCrypto();
if (_globalThisCrypto) { if (_globalThisCrypto) {
webCrypto = _globalThisCrypto; webCrypto = _globalThisCrypto;
return resolve(webCrypto); return resolve(webCrypto);
} }
// We tried to access it both in Node and globally, so bail out // We tried to access it both in Node and globally, so bail out
return reject(new MissingWebCrypto()); return reject(new MissingWebCrypto());
}, });
);
return toResolve; return toResolve;
} }
class MissingWebCrypto extends Error { export class MissingWebCrypto extends Error {
constructor() { constructor() {
const message = "An instance of the Crypto API could not be located"; const message = "An instance of the Crypto API could not be located";
super(message); super(message);
@@ -96,10 +93,10 @@ class MissingWebCrypto extends Error {
} }
} }
// Make it possible to stub return values during testing // Make it possible to stub return values during testing
const _getWebCryptoInternals = { export const _getWebCryptoInternals = {
stubThisGlobalThisCrypto: () => globalThis.crypto, stubThisGlobalThisCrypto: () => globalThis.crypto,
// Make it possible to reset the `webCrypto` at the top of the file // Make it possible to reset the `webCrypto` at the top of the file
setCachedCrypto: (newCrypto: { subtle: SubtleCrypto }) => { setCachedCrypto: (newCrypto: unknown) => {
webCrypto = newCrypto; webCrypto = newCrypto;
}, },
}; };

View File

@@ -1,96 +0,0 @@
import { Buffer } from "buffer/";
import { decode as cborDecode } from "cbor-x";
import { bytesToMultibase, multibaseToBytes } from "did-jwt";
import { getWebCrypto } from "@/libs/crypto/vc/passkeyHelpers";
export const PEER_DID_PREFIX = "did:peer:";
const PEER_DID_MULTIBASE_PREFIX = PEER_DID_PREFIX + "0";
/**
*
*
* similar code is in crowd-funder-for-time-pwa libs/crypto/vc/passkeyDidPeer.ts verifyJwtWebCrypto
*
* @returns {Promise<boolean>}
*/
export async function verifyPeerSignature(
payloadBytes: Buffer,
issuerDid: string,
signatureBytes: Uint8Array,
): Promise<boolean> {
const publicKeyBytes = peerDidToPublicKeyBytes(issuerDid);
const WebCrypto = await getWebCrypto();
const verifyAlgorithm = {
name: "ECDSA",
hash: { name: "SHA-256" },
};
const publicKeyJwk = cborToKeys(publicKeyBytes).publicKeyJwk;
const keyAlgorithm = {
name: "ECDSA",
namedCurve: publicKeyJwk.crv,
};
const publicKeyCryptoKey = await WebCrypto.subtle.importKey(
"jwk",
publicKeyJwk,
keyAlgorithm,
false,
["verify"],
);
const verified = await WebCrypto.subtle.verify(
verifyAlgorithm,
publicKeyCryptoKey,
signatureBytes,
payloadBytes,
);
return verified;
}
export function cborToKeys(publicKeyBytes: Uint8Array) {
const jwkObj = cborDecode(publicKeyBytes);
if (
jwkObj[1] != 2 || // kty "EC"
jwkObj[3] != -7 || // alg "ES256"
jwkObj[-1] != 1 || // crv "P-256"
jwkObj[-2].length != 32 || // x
jwkObj[-3].length != 32 // y
) {
throw new Error("Unable to extract key.");
}
const publicKeyJwk = {
alg: "ES256",
crv: "P-256",
kty: "EC",
x: arrayToBase64Url(jwkObj[-2]),
y: arrayToBase64Url(jwkObj[-3]),
};
const publicKeyBuffer = Buffer.concat([
Buffer.from(jwkObj[-2]),
Buffer.from(jwkObj[-3]),
]);
return { publicKeyJwk, publicKeyBuffer };
}
export function toBase64Url(anythingB64: string) {
return anythingB64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
}
export function arrayToBase64Url(anything: Uint8Array) {
return toBase64Url(Buffer.from(anything).toString("base64"));
}
export function peerDidToPublicKeyBytes(did: string) {
return multibaseToBytes(did.substring(PEER_DID_MULTIBASE_PREFIX.length));
}
export function createPeerDid(publicKeyBytes: Uint8Array) {
// https://github.com/decentralized-identity/veramo/blob/next/packages/did-provider-peer/src/peer-did-provider.ts#L67
//const provider = new PeerDIDProvider({ defaultKms: LOCAL_KMS_NAME });
const methodSpecificId = bytesToMultibase(
publicKeyBytes,
"base58btc",
"p256-pub",
);
return PEER_DID_MULTIBASE_PREFIX + methodSpecificId;
}

View File

@@ -1,112 +0,0 @@
/**
* Verifiable Credential & DID functions, specifically for EndorserSearch.org tools
*
* The goal is to make this folder similar across projects, then move it to a library.
* Other projects: endorser-ch, image-api
*
*/
import * as didJwt from "did-jwt";
import { JWTDecoded } from "did-jwt/lib/JWT";
import { IIdentifier } from "@veramo/core";
import * as u8a from "uint8arrays";
import { createDidPeerJwt } from "@/libs/crypto/vc/passkeyDidPeer";
export const ETHR_DID_PREFIX = "did:ethr:";
/**
* Meta info about a key
*/
export interface KeyMeta {
/**
* Decentralized ID for the key
*/
did: string;
/**
* Stringified IIDentifier object from Veramo
*/
identity?: string;
/**
* The Webauthn credential ID in hex, if this is from a passkey
*/
passkeyCredIdHex?: string;
}
/**
* Tell whether a key is from a passkey
* @param keyMeta contains info about the key, whose passkeyCredIdHex determines if the key is from a passkey
*/
export function isFromPasskey(keyMeta?: KeyMeta): boolean {
return !!keyMeta?.passkeyCredIdHex;
}
export async function createEndorserJwtForKey(
account: KeyMeta,
payload: object,
) {
if (account?.identity) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const identity: IIdentifier = JSON.parse(account.identity!);
const privateKeyHex = identity.keys[0].privateKeyHex;
const signer = await SimpleSigner(privateKeyHex as string);
return didJwt.createJWT(payload, {
issuer: account.did,
signer: signer,
});
} else if (account?.passkeyCredIdHex) {
return createDidPeerJwt(account.did, account.passkeyCredIdHex, payload);
} else {
throw new Error("No identity data found to sign for DID " + account.did);
}
}
/**
* 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(import.meta.env.PRIVATE_KEY)
* signer(data, (err, signature) => {
* ...
* })
*
* @param {String} hexPrivateKey a hex encoded private key
* @return {Function} a configured signer function
*/
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
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
function bytesToHex(b: Uint8Array): string {
return u8a.toString(b, "base16");
}
export function decodeEndorserJwt(jwt: string): JWTDecoded {
return didJwt.decodeJWT(jwt);
}

View File

@@ -1,5 +1,7 @@
import asn1 from "asn1-ber";
import { Buffer } from "buffer/"; import { Buffer } from "buffer/";
import { JWTPayload } from "did-jwt"; import { decode as cborDecode } from "cbor-x";
import { bytesToMultibase, JWTPayload, multibaseToBytes } from "did-jwt";
import { DIDResolutionResult } from "did-resolver"; import { DIDResolutionResult } from "did-resolver";
import { sha256 } from "ethereum-cryptography/sha256.js"; import { sha256 } from "ethereum-cryptography/sha256.js";
import { import {
@@ -19,15 +21,10 @@ import {
PublicKeyCredentialRequestOptionsJSON, PublicKeyCredentialRequestOptionsJSON,
} from "@simplewebauthn/types"; } from "@simplewebauthn/types";
import { AppString } from "@/constants/app"; import { getWebCrypto, unwrapEC2Signature } from "@/libs/crypto/passkeyHelpers";
import { unwrapEC2Signature } from "@/libs/crypto/vc/passkeyHelpers";
import {
arrayToBase64Url,
cborToKeys,
peerDidToPublicKeyBytes,
verifyPeerSignature,
} from "@/libs/crypto/vc/didPeer";
const PEER_DID_PREFIX = "did:peer:";
const PEER_DID_MULTIBASE_PREFIX = PEER_DID_PREFIX + "0";
export interface JWK { export interface JWK {
kty: string; kty: string;
crv: string; crv: string;
@@ -35,12 +32,20 @@ export interface JWK {
y: string; y: string;
} }
function toBase64Url(anythingB64: string) {
return anythingB64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
}
function arrayToBase64Url(anything: Uint8Array) {
return toBase64Url(Buffer.from(anything).toString("base64"));
}
export async function registerCredential(passkeyName?: string) { export async function registerCredential(passkeyName?: string) {
const options: PublicKeyCredentialCreationOptionsJSON = const options: PublicKeyCredentialCreationOptionsJSON =
await generateRegistrationOptions({ await generateRegistrationOptions({
rpName: AppString.APP_NAME, rpName: "Time Safari",
rpID: window.location.hostname, rpID: window.location.hostname,
userName: passkeyName || AppString.APP_NAME + " User", userName: passkeyName || "Time Safari User",
// Don't prompt users for additional information about the authenticator // Don't prompt users for additional information about the authenticator
// (Recommended for smoother UX) // (Recommended for smoother UX)
attestationType: "none", attestationType: "none",
@@ -69,7 +74,7 @@ export async function registerCredential(passkeyName?: string) {
const credIdBase64Url = verification.registrationInfo?.credentialID as string; const credIdBase64Url = verification.registrationInfo?.credentialID as string;
if (attResp.rawId !== credIdBase64Url) { if (attResp.rawId !== credIdBase64Url) {
console.log("Warning! The raw ID does not match the credential ID."); console.log("Warning! The raw ID does not match the credential ID.")
} }
const credIdHex = Buffer.from( const credIdHex = Buffer.from(
base64URLStringToArrayBuffer(credIdBase64Url), base64URLStringToArrayBuffer(credIdBase64Url),
@@ -87,6 +92,21 @@ export async function registerCredential(passkeyName?: string) {
}; };
} }
export function createPeerDid(publicKeyBytes: Uint8Array) {
// https://github.com/decentralized-identity/veramo/blob/next/packages/did-provider-peer/src/peer-did-provider.ts#L67
//const provider = new PeerDIDProvider({ defaultKms: LOCAL_KMS_NAME });
const methodSpecificId = bytesToMultibase(
publicKeyBytes,
"base58btc",
"p256-pub",
);
return PEER_DID_MULTIBASE_PREFIX + methodSpecificId;
}
function peerDidToPublicKeyBytes(did: string) {
return multibaseToBytes(did.substring(PEER_DID_MULTIBASE_PREFIX.length));
}
export class PeerSetup { export class PeerSetup {
public authenticatorData?: ArrayBuffer; public authenticatorData?: ArrayBuffer;
public challenge?: Uint8Array; public challenge?: Uint8Array;
@@ -97,17 +117,13 @@ export class PeerSetup {
issuerDid: string, issuerDid: string,
payload: object, payload: object,
credIdHex: string, credIdHex: string,
expMinutes: number = 1,
) { ) {
const credentialId = arrayBufferToBase64URLString( const credentialId = arrayBufferToBase64URLString(
Buffer.from(credIdHex, "hex").buffer, Buffer.from(credIdHex, "hex").buffer,
); );
const issuedAt = Math.floor(Date.now() / 1000);
const expiryTime = Math.floor(Date.now() / 1000) + expMinutes * 60; // some minutes from now
const fullPayload = { const fullPayload = {
...payload, ...payload,
exp: expiryTime, iat: Math.floor(Date.now() / 1000),
iat: issuedAt,
iss: issuerDid, iss: issuerDid,
}; };
this.challenge = new Uint8Array(Buffer.from(JSON.stringify(fullPayload))); this.challenge = new Uint8Array(Buffer.from(JSON.stringify(fullPayload)));
@@ -143,8 +159,7 @@ export class PeerSetup {
const dataInJwt = { const dataInJwt = {
AuthenticationDataB64URL: authenticatorDataBase64Url, AuthenticationDataB64URL: authenticatorDataBase64Url,
ClientDataJSONB64URL: this.clientDataJsonBase64Url, ClientDataJSONB64URL: this.clientDataJsonBase64Url,
exp: expiryTime, iat: Math.floor(Date.now() / 1000),
iat: issuedAt,
iss: issuerDid, iss: issuerDid,
}; };
const dataInJwtString = JSON.stringify(dataInJwt); const dataInJwtString = JSON.stringify(dataInJwt);
@@ -163,14 +178,10 @@ export class PeerSetup {
issuerDid: string, issuerDid: string,
payload: object, payload: object,
credIdHex: string, credIdHex: string,
expMinutes: number = 1,
) { ) {
const issuedAt = Math.floor(Date.now() / 1000);
const expiryTime = Math.floor(Date.now() / 1000) + expMinutes * 60; // some minutes from now
const fullPayload = { const fullPayload = {
...payload, ...payload,
exp: expiryTime, iat: Math.floor(Date.now() / 1000),
iat: issuedAt,
iss: issuerDid, iss: issuerDid,
}; };
const dataToSignString = JSON.stringify(fullPayload); const dataToSignString = JSON.stringify(fullPayload);
@@ -184,12 +195,12 @@ export class PeerSetup {
allowCredentials: [ allowCredentials: [
{ {
id: credentialId, id: credentialId,
type: "public-key" as const, type: "public-key",
}, },
], ],
challenge: this.challenge.buffer, challenge: this.challenge.buffer,
rpID: window.location.hostname, rpID: window.location.hostname,
userVerification: "preferred" as const, userVerification: "preferred",
}, },
}; };
@@ -198,7 +209,7 @@ export class PeerSetup {
this.authenticatorData = credential?.response.authenticatorData; this.authenticatorData = credential?.response.authenticatorData;
const authenticatorDataBase64Url = arrayBufferToBase64URLString( const authenticatorDataBase64Url = arrayBufferToBase64URLString(
this.authenticatorData as ArrayBuffer, this.authenticatorData,
); );
this.clientDataJsonBase64Url = arrayBufferToBase64URLString( this.clientDataJsonBase64Url = arrayBufferToBase64URLString(
@@ -216,8 +227,7 @@ export class PeerSetup {
const dataInJwt = { const dataInJwt = {
AuthenticationDataB64URL: authenticatorDataBase64Url, AuthenticationDataB64URL: authenticatorDataBase64Url,
ClientDataJSONB64URL: this.clientDataJsonBase64Url, ClientDataJSONB64URL: this.clientDataJsonBase64Url,
exp: expiryTime, iat: Math.floor(Date.now() / 1000),
iat: issuedAt,
iss: issuerDid, iss: issuerDid,
}; };
const dataInJwtString = JSON.stringify(dataInJwt); const dataInJwtString = JSON.stringify(dataInJwt);
@@ -227,9 +237,8 @@ export class PeerSetup {
.replace(/\//g, "_") .replace(/\//g, "_")
.replace(/=+$/, ""); .replace(/=+$/, "");
const origSignature = Buffer.from(credential?.response.signature).toString( const origSignature = Buffer.from(credential?.response.signature)
"base64", .toString("base64")
);
this.signature = origSignature this.signature = origSignature
.replace(/\+/g, "-") .replace(/\+/g, "-")
.replace(/\//g, "_") .replace(/\//g, "_")
@@ -239,9 +248,6 @@ export class PeerSetup {
return jwt; return jwt;
} }
// To use this, add the asn1-ber library and add this import:
// import asn1 from "asn1-ber";
//
// return a low-level signing function, similar to createJWS approach // return a low-level signing function, similar to createJWS approach
// async webAuthnES256KSigner(credentialID: string) { // async webAuthnES256KSigner(credentialID: string) {
// return async (data: string | Uint8Array) => { // return async (data: string | Uint8Array) => {
@@ -298,16 +304,6 @@ export class PeerSetup {
// } // }
} }
export async function createDidPeerJwt(
did: string,
credIdHex: string,
payload: object,
): Promise<string> {
const peerSetup = new PeerSetup();
const jwt = await peerSetup.createJwtNavigator(did, payload, credIdHex);
return jwt;
}
// I'd love to use this but it doesn't verify. // I'd love to use this but it doesn't verify.
// Requires: // Requires:
// npm install @noble/curves // npm install @noble/curves
@@ -380,7 +376,6 @@ export async function verifyJwtSimplewebauthn(
return verification.verified; return verification.verified;
} }
// similar code is in endorser-ch util-crypto.ts verifyPeerSignature
export async function verifyJwtWebCrypto( export async function verifyJwtWebCrypto(
credId: Base64URLString, credId: Base64URLString,
issuerDid: string, issuerDid: string,
@@ -399,10 +394,35 @@ export async function verifyJwtWebCrypto(
// Construct the preimage // Construct the preimage
const preimage = Buffer.concat([authDataFromBase, hash]); const preimage = Buffer.concat([authDataFromBase, hash]);
return verifyPeerSignature(preimage, issuerDid, finalSigBuffer);
const publicKeyBytes = peerDidToPublicKeyBytes(issuerDid);
const WebCrypto = await getWebCrypto();
const verifyAlgorithm = {
name: "ECDSA",
hash: { name: "SHA-256" },
};
const publicKeyJwk = cborToKeys(publicKeyBytes).publicKeyJwk;
const keyAlgorithm = {
name: "ECDSA",
namedCurve: publicKeyJwk.crv,
};
const publicKeyCryptoKey = await WebCrypto.subtle.importKey(
"jwk",
publicKeyJwk,
keyAlgorithm,
false,
["verify"],
);
const verified = await WebCrypto.subtle.verify(
verifyAlgorithm,
publicKeyCryptoKey,
finalSigBuffer,
preimage,
);
return verified;
} }
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async function peerDidToDidDocument(did: string): Promise<DIDResolutionResult> { async function peerDidToDidDocument(did: string): Promise<DIDResolutionResult> {
if (!did.startsWith("did:peer:0z")) { if (!did.startsWith("did:peer:0z")) {
throw new Error( throw new Error(
@@ -411,21 +431,13 @@ async function peerDidToDidDocument(did: string): Promise<DIDResolutionResult> {
} }
// this is basically hard-coded from https://www.w3.org/TR/did-core/#example-various-verification-method-types // this is basically hard-coded from https://www.w3.org/TR/did-core/#example-various-verification-method-types
// (another reference is the @aviarytech/did-peer resolver) // (another reference is the @aviarytech/did-peer resolver)
/**
* Looks like JsonWebKey2020 isn't too difficult:
* - change context security/suites link to jws-2020/v1
* - change publicKeyMultibase to publicKeyJwk generated with cborToKeys
* - change type to JsonWebKey2020
*/
const id = did.split(":")[2]; const id = did.split(":")[2];
const multibase = id.slice(1); const multibase = id.slice(1);
const encnumbasis = multibase.slice(1); const encnumbasis = multibase.slice(1);
const didDocument = { const didDocument = {
"@context": [ "@context": [
"https://www.w3.org/ns/did/v1", "https://www.w3.org/ns/did/v1",
"https://w3id.org/security/suites/secp256k1-2019/v1", "https://w3id.org/security/suites/jws-2020/v1",
], ],
assertionMethod: [did + "#" + encnumbasis], assertionMethod: [did + "#" + encnumbasis],
authentication: [did + "#" + encnumbasis], authentication: [did + "#" + encnumbasis],
@@ -451,15 +463,12 @@ async function peerDidToDidDocument(did: string): Promise<DIDResolutionResult> {
} }
// convert COSE public key to PEM format // convert COSE public key to PEM format
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function COSEtoPEM(cose: Buffer) { function COSEtoPEM(cose: Buffer) {
// const alg = cose.get(3); // Algorithm // const alg = cose.get(3); // Algorithm
const x = cose[-2]; // x-coordinate const x = cose[-2]; // x-coordinate
const y = cose[-3]; // y-coordinate const y = cose[-3]; // y-coordinate
// Ensure the coordinates are in the correct format // Ensure the coordinates are in the correct format
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error because it complains about the type of x and y
const pubKeyBuffer = Buffer.concat([Buffer.from([0x04]), x, y]); const pubKeyBuffer = Buffer.concat([Buffer.from([0x04]), x, y]);
// Convert to PEM format // Convert to PEM format
@@ -470,7 +479,6 @@ ${pubKeyBuffer.toString("base64")}
return pem; return pem;
} }
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function base64urlDecode(input: string) { function base64urlDecode(input: string) {
input = input.replace(/-/g, "+").replace(/_/g, "/"); input = input.replace(/-/g, "+").replace(/_/g, "/");
const pad = input.length % 4 === 0 ? "" : "====".slice(input.length % 4); const pad = input.length % 4 === 0 ? "" : "====".slice(input.length % 4);
@@ -482,14 +490,13 @@ function base64urlDecode(input: string) {
return bytes.buffer; return bytes.buffer;
} }
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function base64urlEncode(buffer: ArrayBuffer) { function base64urlEncode(buffer: ArrayBuffer) {
const str = String.fromCharCode(...new Uint8Array(buffer)); const str = String.fromCharCode(...new Uint8Array(buffer));
return btoa(str).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); return btoa(str).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
} }
// from @simplewebauthn/browser // from @simplewebauthn/browser
function arrayBufferToBase64URLString(buffer: ArrayBuffer) { function arrayBufferToBase64URLString(buffer) {
const bytes = new Uint8Array(buffer); const bytes = new Uint8Array(buffer);
let str = ""; let str = "";
for (const charCode of bytes) { for (const charCode of bytes) {
@@ -513,7 +520,31 @@ function base64URLStringToArrayBuffer(base64URLString: string) {
return buffer; return buffer;
} }
// eslint-disable-next-line @typescript-eslint/no-unused-vars function cborToKeys(publicKeyBytes: Uint8Array) {
const jwkObj = cborDecode(publicKeyBytes);
if (
jwkObj[1] != 2 || // kty "EC"
jwkObj[3] != -7 || // alg "ES256"
jwkObj[-1] != 1 || // crv "P-256"
jwkObj[-2].length != 32 || // x
jwkObj[-3].length != 32 // y
) {
throw new Error("Unable to extract key.");
}
const publicKeyJwk = {
alg: "ES256",
crv: "P-256",
kty: "EC",
x: arrayToBase64Url(jwkObj[-2]),
y: arrayToBase64Url(jwkObj[-3]),
};
const publicKeyBuffer = Buffer.concat([
Buffer.from(jwkObj[-2]),
Buffer.from(jwkObj[-3]),
]);
return { publicKeyJwk, publicKeyBuffer };
}
async function pemToCryptoKey(pem: string) { async function pemToCryptoKey(pem: string) {
const binaryDerString = atob( const binaryDerString = atob(
pem pem

View File

@@ -1,14 +1,19 @@
import { Axios, AxiosRequestConfig, AxiosResponse } from "axios"; import {
Axios,
AxiosRequestConfig,
AxiosResponse,
RawAxiosRequestHeaders,
} from "axios";
import * as didJwt from "did-jwt";
import { LRUCache } from "lru-cache"; import { LRUCache } from "lru-cache";
import * as R from "ramda"; import * as R from "ramda";
import { IIdentifier } from "@veramo/core";
import { DEFAULT_IMAGE_API_SERVER } from "@/constants/app"; import { DEFAULT_IMAGE_API_SERVER } from "@/constants/app";
import { Contact } from "@/db/tables/contacts"; import { Contact } from "@/db/tables/contacts";
import { accessToken } from "@/libs/crypto"; import { accessToken, SimpleSigner } from "@/libs/crypto";
import { db, NonsensitiveDexie } from "@/db/index"; import { NonsensitiveDexie } from "@/db/index";
import { getAccount, getPasskeyExpirationSeconds } from "@/libs/util"; import { getIdentity } from "@/libs/util";
import { createEndorserJwtForKey, KeyMeta } from "@/libs/crypto/vc";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
export const SCHEMA_ORG_CONTEXT = "https://schema.org"; export const SCHEMA_ORG_CONTEXT = "https://schema.org";
// the object in RegisterAction claims // the object in RegisterAction claims
@@ -448,75 +453,28 @@ export function didInfo(
return didInfoForContact(did, activeDid, contact, allMyDids).displayName; return didInfoForContact(did, activeDid, contact, allMyDids).displayName;
} }
let passkeyAccessToken: string = ""; async function getHeaders(identity: IIdentifier | null) {
let passkeyTokenExpirationEpochSeconds: number = 0; const headers: RawAxiosRequestHeaders = {
export function clearPasskeyToken() {
passkeyAccessToken = "";
passkeyTokenExpirationEpochSeconds = 0;
}
export function tokenExpiryTimeDescription() {
if (
!passkeyAccessToken ||
passkeyTokenExpirationEpochSeconds < new Date().getTime() / 1000
) {
return "Token has expired";
} else {
return (
"Token expires at " +
new Date(passkeyTokenExpirationEpochSeconds * 1000).toLocaleString()
);
}
}
/**
* Get the headers for a request, potentially including Authorization
*/
export async function getHeaders(did?: string) {
const headers: { "Content-Type": string; Authorization?: string } = {
"Content-Type": "application/json", "Content-Type": "application/json",
}; };
if (did) { if (identity) {
let token; const token = await accessToken(identity);
const account = await getAccount(did);
if (account?.passkeyCredIdHex) {
if (
passkeyAccessToken &&
passkeyTokenExpirationEpochSeconds > Date.now() / 1000
) {
// there's an active current passkey token
token = passkeyAccessToken;
} else {
// there's no current passkey token or it's expired
token = await accessToken(did);
passkeyAccessToken = token;
const passkeyExpirationSeconds = await getPasskeyExpirationSeconds();
passkeyTokenExpirationEpochSeconds =
Date.now() / 1000 + passkeyExpirationSeconds;
}
} else {
token = await accessToken(did);
}
headers["Authorization"] = "Bearer " + token; headers["Authorization"] = "Bearer " + token;
} else {
// it's often OK to request without auth; we assume necessary checks are done earlier
} }
return headers; return headers;
} }
/** /**
* @param handleId nullable, in which case "undefined" will be returned * @param handleId nullable, in which case "undefined" will be returned
* @param requesterDid optional, in which case no private info will be returned * @param identity nullable, in which case no private info will be returned
* @param axios * @param axios
* @param apiServer * @param apiServer
*/ */
export async function getPlanFromCache( export async function getPlanFromCache(
handleId: string | null, handleId: string | null,
identity: IIdentifier | null,
axios: Axios, axios: Axios,
apiServer: string, apiServer: string,
requesterDid?: string,
): Promise<PlanSummaryRecord | undefined> { ): Promise<PlanSummaryRecord | undefined> {
if (!handleId) { if (!handleId) {
return undefined; return undefined;
@@ -527,7 +485,7 @@ export async function getPlanFromCache(
apiServer + apiServer +
"/api/v2/report/plans?handleId=" + "/api/v2/report/plans?handleId=" +
encodeURIComponent(handleId); encodeURIComponent(handleId);
const headers = await getHeaders(requesterDid); const headers = await getHeaders(identity);
try { try {
const resp = await axios.get(url, { headers }); const resp = await axios.get(url, { headers });
if (resp.status === 200 && resp.data?.data?.length > 0) { if (resp.status === 200 && resp.data?.data?.length > 0) {
@@ -561,9 +519,18 @@ export async function setPlanInCache(
} }
/** /**
* Construct GiveAction VC for submission to server * For result, see https://api.endorser.ch/api-docs/#/claims/post_api_v2_claim
*
* @param identity
* @param fromDid may be null
* @param toDid
* @param description may be null; should have this or amount
* @param amount may be null; should have this or description
*/ */
export function constructGive( export async function createAndSubmitGive(
axios: Axios,
apiServer: string,
identity: IIdentifier,
fromDid?: string | null, fromDid?: string | null,
toDid?: string, toDid?: string,
description?: string, description?: string,
@@ -573,9 +540,9 @@ export function constructGive(
fulfillsOfferHandleId?: string, fulfillsOfferHandleId?: string,
isTrade: boolean = false, isTrade: boolean = false,
imageUrl?: string, imageUrl?: string,
): GiveVerifiableCredential { ): Promise<CreateAndSubmitClaimResult> {
const vcClaim: GiveVerifiableCredential = { const vcClaim: GiveVerifiableCredential = {
"@context": SCHEMA_ORG_CONTEXT, "@context": "https://schema.org",
"@type": "GiveAction", "@type": "GiveAction",
recipient: toDid ? { identifier: toDid } : undefined, recipient: toDid ? { identifier: toDid } : undefined,
agent: fromDid ? { identifier: fromDid } : undefined, agent: fromDid ? { identifier: fromDid } : undefined,
@@ -602,46 +569,9 @@ export function constructGive(
if (imageUrl) { if (imageUrl) {
vcClaim.image = imageUrl; vcClaim.image = imageUrl;
} }
return vcClaim;
}
/**
* For result, see https://api.endorser.ch/api-docs/#/claims/post_api_v2_claim
*
* @param identity
* @param fromDid may be null
* @param toDid
* @param description may be null; should have this or amount
* @param amount may be null; should have this or description
*/
export async function createAndSubmitGive(
axios: Axios,
apiServer: string,
issuerDid: string,
fromDid?: string | null,
toDid?: string,
description?: string,
amount?: number,
unitCode?: string,
fulfillsProjectHandleId?: string,
fulfillsOfferHandleId?: string,
isTrade: boolean = false,
imageUrl?: string,
): Promise<CreateAndSubmitClaimResult> {
const vcClaim = constructGive(
fromDid,
toDid,
description,
amount,
unitCode,
fulfillsProjectHandleId,
fulfillsOfferHandleId,
isTrade,
imageUrl,
);
return createAndSubmitClaim( return createAndSubmitClaim(
vcClaim as GenericCredWrapper, vcClaim as GenericCredWrapper,
issuerDid, identity,
apiServer, apiServer,
axios, axios,
); );
@@ -659,7 +589,7 @@ export async function createAndSubmitGive(
export async function createAndSubmitOffer( export async function createAndSubmitOffer(
axios: Axios, axios: Axios,
apiServer: string, apiServer: string,
issuerDid: string, identity: IIdentifier,
description?: string, description?: string,
amount?: number, amount?: number,
unitCode?: string, unitCode?: string,
@@ -668,9 +598,9 @@ export async function createAndSubmitOffer(
fulfillsProjectHandleId?: string, fulfillsProjectHandleId?: string,
): Promise<CreateAndSubmitClaimResult> { ): Promise<CreateAndSubmitClaimResult> {
const vcClaim: OfferVerifiableCredential = { const vcClaim: OfferVerifiableCredential = {
"@context": SCHEMA_ORG_CONTEXT, "@context": "https://schema.org",
"@type": "Offer", "@type": "Offer",
offeredBy: { identifier: issuerDid }, offeredBy: { identifier: identity.did },
validThrough: expirationDate || undefined, validThrough: expirationDate || undefined,
}; };
if (amount) { if (amount) {
@@ -694,7 +624,7 @@ export async function createAndSubmitOffer(
} }
return createAndSubmitClaim( return createAndSubmitClaim(
vcClaim as GenericCredWrapper, vcClaim as GenericCredWrapper,
issuerDid, identity,
apiServer, apiServer,
axios, axios,
); );
@@ -702,7 +632,7 @@ export async function createAndSubmitOffer(
// similar logic is found in endorser-mobile // similar logic is found in endorser-mobile
export const createAndSubmitConfirmation = async ( export const createAndSubmitConfirmation = async (
issuerDid: string, identifier: IIdentifier,
claim: GenericVerifiableCredential, claim: GenericVerifiableCredential,
lastClaimId: string, // used to set the lastClaimId lastClaimId: string, // used to set the lastClaimId
handleId: string | undefined, handleId: string | undefined,
@@ -715,16 +645,16 @@ export const createAndSubmitConfirmation = async (
), ),
); );
const confirmationClaim: GenericVerifiableCredential = { const confirmationClaim: GenericVerifiableCredential = {
"@context": SCHEMA_ORG_CONTEXT, "@context": "https://schema.org",
"@type": "AgreeAction", "@type": "AgreeAction",
object: goodClaim, object: goodClaim,
}; };
return createAndSubmitClaim(confirmationClaim, issuerDid, apiServer, axios); return createAndSubmitClaim(confirmationClaim, identifier, apiServer, axios);
}; };
export async function createAndSubmitClaim( export async function createAndSubmitClaim(
vcClaim: GenericVerifiableCredential, vcClaim: GenericVerifiableCredential,
issuerDid: string, identity: IIdentifier,
apiServer: string, apiServer: string,
axios: Axios, axios: Axios,
): Promise<CreateAndSubmitClaimResult> { ): Promise<CreateAndSubmitClaimResult> {
@@ -737,15 +667,34 @@ export async function createAndSubmitClaim(
}, },
}; };
const vcJwt: string = await createEndorserJwtForDid(issuerDid, vcPayload); // Create a signature using private key of identity
const firstKey = identity.keys[0];
const privateKeyHex = firstKey?.privateKeyHex;
if (!privateKeyHex) {
throw {
error: "No private key",
message: `Your identifier ${identity.did} is not configured correctly. Use a different identifier.`,
};
}
const signer = await SimpleSigner(privateKeyHex);
// Create a JWT for the request
const vcJwt: string = await didJwt.createJWT(vcPayload, {
issuer: identity.did,
signer,
});
// Make the xhr request payload // Make the xhr request payload
const payload = JSON.stringify({ jwtEncoded: vcJwt }); const payload = JSON.stringify({ jwtEncoded: vcJwt });
const url = `${apiServer}/api/v2/claim`; const url = `${apiServer}/api/v2/claim`;
const token = await accessToken(identity);
const response = await axios.post(url, payload, { const response = await axios.post(url, payload, {
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
Authorization: `Bearer ${token}`,
}, },
}); });
@@ -767,14 +716,6 @@ export async function createAndSubmitClaim(
} }
} }
export async function createEndorserJwtForDid(
issuerDid: string,
payload: object,
) {
const account = await getAccount(issuerDid);
return createEndorserJwtForKey(account as KeyMeta, payload);
}
/** /**
* An AcceptAction is when someone accepts some contract or pledge. * An AcceptAction is when someone accepts some contract or pledge.
* *
@@ -978,31 +919,18 @@ export const bvcMeetingJoinClaim = (did: string, startTime: string) => {
}; };
}; };
export async function createEndorserJwtVcFromClaim(
issuerDid: string,
claim: object,
) {
// Make a payload for the claim
const vcPayload = {
vc: {
"@context": ["https://www.w3.org/2018/credentials/v1"],
type: ["VerifiableCredential"],
credentialSubject: claim,
},
};
return createEndorserJwtForDid(issuerDid, vcPayload);
}
export async function register( export async function register(
activeDid: string, activeDid: string,
apiServer: string, apiServer: string,
axios: Axios, axios: Axios,
contact: Contact, contact: Contact,
) { ) {
const identity = await getIdentity(activeDid);
const vcClaim: RegisterVerifiableCredential = { const vcClaim: RegisterVerifiableCredential = {
"@context": SCHEMA_ORG_CONTEXT, "@context": "https://schema.org",
"@type": "RegisterAction", "@type": "RegisterAction",
agent: { identifier: activeDid }, agent: { identifier: identity.did },
object: SERVICE_ID, object: SERVICE_ID,
participant: { identifier: contact.did }, participant: { identifier: contact.did },
}; };
@@ -1015,10 +943,26 @@ export async function register(
}, },
}; };
// Create a signature using private key of identity // Create a signature using private key of identity
const vcJwt = await createEndorserJwtForDid(activeDid, vcPayload); if (identity.keys[0].privateKeyHex == null) {
return { error: "Private key not found." };
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
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 url = apiServer + "/api/v2/claim"; const url = apiServer + "/api/v2/claim";
const resp = await axios.post(url, { jwtEncoded: vcJwt }); const headers = await getHeaders(identity);
const resp = await axios.post(url, payload, { headers });
if (resp.data?.success?.handleId) { if (resp.data?.success?.handleId) {
return { success: true }; return { success: true };
} else if (resp.data?.success?.embeddedRecordError) { } else if (resp.data?.success?.embeddedRecordError) {
@@ -1047,7 +991,8 @@ export async function setVisibilityUtil(
} }
const url = const url =
apiServer + "/api/report/" + (visibility ? "canSeeMe" : "cannotSeeMe"); apiServer + "/api/report/" + (visibility ? "canSeeMe" : "cannotSeeMe");
const headers = await getHeaders(activeDid); const identity = await getIdentity(activeDid);
const headers = await getHeaders(identity);
const payload = JSON.stringify({ did: contact.did }); const payload = JSON.stringify({ did: contact.did });
try { try {
@@ -1076,16 +1021,16 @@ export async function setVisibilityUtil(
* *
* @param apiServer endorser server URL string * @param apiServer endorser server URL string
* @param axios Axios instance * @param axios Axios instance
* @param {string} issuerDid - The DID for which to check rate limits. * @param {IIdentifier} identity - The identity object to check rate limits for.
* @returns {Promise<AxiosResponse>} The Axios response object. * @returns {Promise<AxiosResponse>} The Axios response object.
*/ */
export async function fetchEndorserRateLimits( export async function fetchEndorserRateLimits(
apiServer: string, apiServer: string,
axios: Axios, axios: Axios,
issuerDid: string, identity: IIdentifier,
) { ) {
const url = `${apiServer}/api/report/rateLimits`; const url = `${apiServer}/api/report/rateLimits`;
const headers = await getHeaders(issuerDid); const headers = await getHeaders(identity);
return await axios.get(url, { headers } as AxiosRequestConfig); return await axios.get(url, { headers } as AxiosRequestConfig);
} }
@@ -1094,11 +1039,15 @@ export async function fetchEndorserRateLimits(
* *
* @param apiServer image server URL string * @param apiServer image server URL string
* @param axios Axios instance * @param axios Axios instance
* @param {string} issuerDid - The DID for which to check rate limits. * @param {IIdentifier} identity - The identity object to check rate limits for.
* @returns {Promise<AxiosResponse>} The Axios response object. * @returns {Promise<AxiosResponse>} The Axios response object.
*/ */
export async function fetchImageRateLimits(axios: Axios, issuerDid: string) { export async function fetchImageRateLimits(
apiServer: string,
axios: Axios,
identity: IIdentifier,
) {
const url = DEFAULT_IMAGE_API_SERVER + "/image-limits"; const url = DEFAULT_IMAGE_API_SERVER + "/image-limits";
const headers = await getHeaders(issuerDid); const headers = await getHeaders(identity);
return await axios.get(url, { headers } as AxiosRequestConfig); return await axios.get(url, { headers } as AxiosRequestConfig);
} }

View File

@@ -1,23 +1,19 @@
// many of these are also found in endorser-mobile utility.ts // many of these are also found in endorser-mobile utility.ts
import axios, { AxiosResponse } from "axios"; import axios, { AxiosResponse } from "axios";
import { IIdentifier } from "@veramo/core";
import { useClipboard } from "@vueuse/core"; import { useClipboard } from "@vueuse/core";
import { DEFAULT_PUSH_SERVER } from "@/constants/app"; import { DEFAULT_PUSH_SERVER } from "@/constants/app";
import { accountsDB, db } from "@/db/index"; import { accountsDB, db } from "@/db/index";
import { Account } from "@/db/tables/accounts"; import { Account } from "@/db/tables/accounts";
import {DEFAULT_PASSKEY_EXPIRATION_MINUTES, MASTER_SETTINGS_KEY} from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { deriveAddress, generateSeed, newIdentifier } from "@/libs/crypto"; import { deriveAddress, generateSeed, newIdentifier } from "@/libs/crypto";
import { GenericCredWrapper, containsHiddenDid } from "@/libs/endorserServer"; import { GenericCredWrapper, containsHiddenDid } from "@/libs/endorserServer";
import * as serverUtil from "@/libs/endorserServer"; import * as serverUtil from "@/libs/endorserServer";
import { registerCredential } from "@/libs/crypto/vc/passkeyDidPeer";
import { Buffer } from "buffer";
import { KeyMeta } from "@/libs/crypto/vc";
import { createPeerDid } from "@/libs/crypto/vc/didPeer";
export const PRIVACY_MESSAGE = export const PRIVACY_MESSAGE =
"The data you send will be visible to the world -- except: your IDs and the IDs of anyone you tag will stay private, only visible to them and others you explicitly allow."; "The data you send be visible to the world -- except: your IDs and the IDs of anyone you tag will stay private, only visible to those you allow.";
/* eslint-disable prettier/prettier */ /* eslint-disable prettier/prettier */
export const UNIT_SHORT: Record<string, string> = { export const UNIT_SHORT: Record<string, string> = {
@@ -197,17 +193,20 @@ export function findAllVisibleToDids(
* *
**/ **/
export interface AccountKeyInfo extends Account, KeyMeta {} export const getIdentity = async (activeDid: string): Promise<IIdentifier> => {
export const getAccount = async (
activeDid: string,
): Promise<AccountKeyInfo | undefined> => {
await accountsDB.open(); await accountsDB.open();
const account = (await accountsDB.accounts const account = (await accountsDB.accounts
.where("did") .where("did")
.equals(activeDid) .equals(activeDid)
.first()) as Account; .first()) as Account;
return account; const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error(
`Attempted to load identity ${activeDid} but no identifier was found`,
);
}
return identity;
}; };
/** /**
@@ -240,47 +239,6 @@ export const generateSaveAndActivateIdentity = async (): Promise<string> => {
return newId.did; return newId.did;
}; };
export const registerAndSavePasskey = async (
keyName: string,
): Promise<Account> => {
const cred = await registerCredential(keyName);
const publicKeyBytes = cred.publicKeyBytes;
const did = createPeerDid(publicKeyBytes as Uint8Array);
const passkeyCredIdHex = cred.credIdHex as string;
const account = {
dateCreated: new Date().toISOString(),
did,
passkeyCredIdHex,
publicKeyHex: Buffer.from(publicKeyBytes).toString("hex"),
};
await accountsDB.open();
await accountsDB.accounts.add(account);
return account;
};
export const registerSaveAndActivatePasskey = async (
keyName: string,
): Promise<Account> => {
const account = await registerAndSavePasskey(keyName);
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, {
activeDid: account.did,
});
return account;
};
export const getPasskeyExpirationSeconds = async (): Promise<number> => {
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
const passkeyExpirationSeconds =
(settings?.passkeyExpirationMinutes ?? DEFAULT_PASSKEY_EXPIRATION_MINUTES) *
60;
return passkeyExpirationSeconds;
};
export const sendTestThroughPushServer = async ( export const sendTestThroughPushServer = async (
subscriptionJSON: PushSubscriptionJSON, subscriptionJSON: PushSubscriptionJSON,
skipFilter: boolean, skipFilter: boolean,

View File

@@ -38,11 +38,6 @@ const routes: Array<RouteRecordRaw> = [
name: "claim", name: "claim",
component: () => import("../views/ClaimView.vue"), component: () => import("../views/ClaimView.vue"),
}, },
{
path: "/claim-add-raw/:id?",
name: "claim-add-raw",
component: () => import("../views/ClaimAddRawView.vue"),
},
{ {
path: "/confirm-contact", path: "/confirm-contact",
name: "confirm-contact", name: "confirm-contact",

View File

@@ -6,9 +6,6 @@ import { SERVICE_ID } from "../libs/endorserServer";
import { deriveAddress, newIdentifier } from "../libs/crypto"; import { deriveAddress, newIdentifier } from "../libs/crypto";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
/**
* Get User #0 to sign & submit a RegisterAction for the user's activeDid.
*/
export async function testServerRegisterUser() { export async function testServerRegisterUser() {
const testUser0Mnem = 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"; "seminar accuse mystery assist delay law thing deal image undo guard initial shallow wrestle list fragile borrow velvet tomorrow awake explain test offer control";

View File

@@ -152,7 +152,7 @@
<div class="text-blue-500 text-sm font-bold"> <div class="text-blue-500 text-sm font-bold">
<router-link :to="{ path: '/did/' + encodeURIComponent(activeDid) }"> <router-link :to="{ path: '/did/' + encodeURIComponent(activeDid) }">
Your Activity Activity
</router-link> </router-link>
</div> </div>
</div> </div>
@@ -216,6 +216,7 @@
<div class="mb-2 font-bold">Location</div> <div class="mb-2 font-bold">Location</div>
<router-link <router-link
:to="{ name: 'search-area' }" :to="{ name: 'search-area' }"
v-if="activeDid"
class="block w-full text-center text-m bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-2 mt-6" class="block w-full text-center text-m bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-2 mt-6"
> >
Set Search Area Set Search Area
@@ -313,7 +314,7 @@
> >
Advanced Advanced
</h3> </h3>
<div v-if="showAdvanced || showGeneralAdvanced"> <div v-if="showAdvanced">
<p class="text-rose-600 mb-8"> <p class="text-rose-600 mb-8">
Beware: the features here can be confusing and even change data in ways Beware: the features here can be confusing and even change data in ways
you do not expect. But we support your freedom! you do not expect. But we support your freedom!
@@ -358,7 +359,6 @@
<div class="text-slate-500 text-sm font-bold">Derivation Path</div> <div class="text-slate-500 text-sm font-bold">Derivation Path</div>
<div <div
v-if="derivationPath"
class="text-sm text-slate-500 flex justify-start items-center mb-1" class="text-sm text-slate-500 flex justify-start items-center mb-1"
> >
<code class="truncate">{{ derivationPath }}</code> <code class="truncate">{{ derivationPath }}</code>
@@ -375,12 +375,6 @@
</button> </button>
<span v-show="showDerCopy">Copied</span> <span v-show="showDerCopy">Copied</span>
</div> </div>
<div
v-else
class="text-sm text-slate-500 flex justify-start items-center mb-1"
>
(none)
</div>
</div> </div>
<!-- id used by puppeteer test script --> <!-- id used by puppeteer test script -->
@@ -392,27 +386,6 @@
Switch Identifier Switch Identifier
</router-link> </router-link>
<div class="mt-4">
<h2 class="text-slate-500 text-sm font-bold">
Contacts & Settings Database
</h2>
<div class="ml-4 mt-2">
Import
<input type="file" @change="uploadImportFile" class="ml-2" />
<div v-if="showContactImport()">
<button
class="block text-center text-md bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-6"
@click="confirmSubmitImportFile()"
>
Import Settings & Contacts
<br />
(excluding Identifier Data)
</button>
</div>
</div>
</div>
<label <label
for="toggleShowAmounts" for="toggleShowAmounts"
class="flex items-center justify-between cursor-pointer my-4" class="flex items-center justify-between cursor-pointer my-4"
@@ -610,6 +583,27 @@
</div> </div>
</label> </label>
<div class="mt-4">
<h2 class="text-slate-500 text-sm font-bold">
Contacts & Settings Database
</h2>
<div class="ml-4 mt-2">
Import
<input type="file" @change="uploadImportFile" class="ml-2" />
<div v-if="showContactImport()">
<button
class="block text-center text-md bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-6"
@click="confirmSubmitImportFile()"
>
Import Settings & Contacts
<br />
(excluding Identifier Data)
</button>
</div>
</div>
</div>
<div class="flex mt-4"> <div class="flex mt-4">
<button> <button>
<router-link <router-link
@@ -620,96 +614,49 @@
</router-link> </router-link>
</button> </button>
</div> </div>
<div class="flex justify-between">
<span>
<span class="text-slate-500 text-sm font-bold mb-2">
Passkey Expiration Minutes
</span>
<br />
<span class="text-sm ml-2">
{{ passkeyExpirationDescription }}
</span>
</span>
<div class="relative ml-2">
<input
type="number"
class="border border-slate-400 rounded px-2 py-2 text-center w-20"
v-model="passkeyExpirationMinutes"
@change="updatePasskeyExpiration"
/>
</div>
</div>
<label
for="toggleShowGeneralAdvanced"
class="flex items-center justify-between cursor-pointer mt-4"
@click="toggleShowGeneralAdvanced"
>
<!-- label -->
<span class="text-slate-500 text-sm font-bold">
Show All General Advanced Functions
</span>
<!-- toggle -->
<div class="relative ml-2">
<!-- input -->
<input
type="checkbox"
v-model="showGeneralAdvanced"
class="sr-only"
/>
<!-- line -->
<div class="block bg-slate-500 w-14 h-8 rounded-full" />
<!-- dot -->
<div
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
/>
</div>
</label>
</div> </div>
</section> </section>
</template> </template>
<script lang="ts"> <script lang="ts">
import { AxiosError } from "axios"; import { AxiosError } from "axios";
import { Buffer } from "buffer/";
import Dexie from "dexie"; import Dexie from "dexie";
import "dexie-export-import"; import "dexie-export-import";
import { ImportProgress } from "dexie-export-import/dist/import"; import { ImportProgress } from "dexie-export-import/dist/import";
import { IIdentifier } from "@veramo/core";
import { ref } from "vue"; import { ref } from "vue";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { useClipboard } from "@vueuse/core"; import { useClipboard } from "@vueuse/core";
import EntityIcon from "@/components/EntityIcon.vue";
import ImageMethodDialog from "@/components/ImageMethodDialog.vue"; import ImageMethodDialog from "@/components/ImageMethodDialog.vue";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
import TopMessage from "@/components/TopMessage.vue"; import TopMessage from "@/components/TopMessage.vue";
import { import {
AppString, DEFAULT_ENDORSER_API_SERVER, AppString,
DEFAULT_IMAGE_API_SERVER, DEFAULT_IMAGE_API_SERVER,
DEFAULT_PUSH_SERVER, DEFAULT_PUSH_SERVER,
IMAGE_TYPE_PROFILE, IMAGE_TYPE_PROFILE,
NotificationIface, NotificationIface,
} from "@/constants/app"; } from "@/constants/app";
import { db, accountsDB } from "@/db/index"; import { db, accountsDB } from "@/db/index";
import { Account } from "@/db/tables/accounts"; import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";
import { IIdentifier } from "@veramo/core";
import { import {
DEFAULT_PASSKEY_EXPIRATION_MINUTES,
MASTER_SETTINGS_KEY,
Settings,
} from "@/db/tables/settings";
import {
clearPasskeyToken,
ErrorResponse, ErrorResponse,
EndorserRateLimits, EndorserRateLimits,
ImageRateLimits,
fetchEndorserRateLimits, fetchEndorserRateLimits,
fetchImageRateLimits, fetchImageRateLimits,
getHeaders,
ImageRateLimits,
tokenExpiryTimeDescription,
} from "@/libs/endorserServer"; } from "@/libs/endorserServer";
import { getAccount } from "@/libs/util"; import { Buffer } from "buffer/";
import EntityIcon from "@/components/EntityIcon.vue";
interface IAccount {
did: string;
publicKeyHex: string;
privateHex?: string;
derivationPath: string;
}
const inputImportFileNameRef = ref<Blob>(); const inputImportFileNameRef = ref<Blob>();
@@ -730,35 +677,31 @@ export default class AccountViewView extends Vue {
downloadUrl = ""; // because DuckDuckGo doesn't download on automated call to "click" on the anchor downloadUrl = ""; // because DuckDuckGo doesn't download on automated call to "click" on the anchor
endorserLimits: EndorserRateLimits | null = null; endorserLimits: EndorserRateLimits | null = null;
givenName = ""; givenName = "";
hideRegisterPromptOnNewContact = false;
imageLimits: ImageRateLimits | null = null; imageLimits: ImageRateLimits | null = null;
imageServer = "";
isRegistered = false; isRegistered = false;
isSubscribed = false; isSubscribed = false;
limitsMessage = "";
loadingLimits = false;
notificationMaybeChanged = false; notificationMaybeChanged = false;
passkeyExpirationDescription = "";
passkeyExpirationMinutes = DEFAULT_PASSKEY_EXPIRATION_MINUTES;
previousPasskeyExpirationMinutes = DEFAULT_PASSKEY_EXPIRATION_MINUTES;
profileImageUrl?: string; profileImageUrl?: string;
publicHex = ""; publicHex = "";
publicBase64 = ""; publicBase64 = "";
showAdvanced = false; showLargeIdenticonId?: string;
showB64Copy = false; showLargeIdenticonUrl?: string;
webPushServer = "";
webPushServerInput = "";
limitsMessage = "";
loadingLimits = false;
showContactGives = false; showContactGives = false;
showDidCopy = false; showDidCopy = false;
showDerCopy = false; showDerCopy = false;
showGeneralAdvanced = false; showB64Copy = false;
showLargeIdenticonId?: string;
showLargeIdenticonUrl?: string;
showPubCopy = false; showPubCopy = false;
showAdvanced = false;
hideRegisterPromptOnNewContact = false;
showShortcutBvc = false; showShortcutBvc = false;
subscription: PushSubscription | null = null; subscription: PushSubscription | null = null;
warnIfProdServer = false; warnIfProdServer = false;
warnIfTestServer = false; warnIfTestServer = false;
webPushServer = "";
webPushServerInput = "";
/** /**
* Async function executed when the component is mounted. * Async function executed when the component is mounted.
@@ -769,36 +712,25 @@ export default class AccountViewView extends Vue {
*/ */
async mounted() { async mounted() {
try { try {
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
// Initialize component state with values from the database or defaults // Initialize component state with values from the database or defaults
await this.initializeState(); this.initializeState(settings);
await this.processIdentity();
this.passkeyExpirationDescription = tokenExpiryTimeDescription(); // Get and process the identity
const identity = await this.getIdentity(this.activeDid);
if (identity) {
this.processIdentity(identity);
}
/**
* Beware! I've seen where this "ready" never resolves.
*/
const registration = await navigator.serviceWorker.ready; const registration = await navigator.serviceWorker.ready;
this.subscription = await registration.pushManager.getSubscription(); this.subscription = await registration.pushManager.getSubscription();
this.isSubscribed = !!this.subscription; this.isSubscribed = !!this.subscription;
console.log("Got to the end of 'mounted' call.");
/**
* Beware! I've seen where we never get to this point because "ready" never resolves.
*/
} catch (error) { } catch (error) {
console.error( console.error("Mount error:", error);
"Telling user to clear cache at page create because:", this.handleError(error);
error,
);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Loading Account",
text: "Clear your cache and start over (after data backup).",
},
-1,
);
} }
} }
@@ -810,12 +742,9 @@ export default class AccountViewView extends Vue {
/** /**
* Initializes component state with values from the database or defaults. * Initializes component state with values from the database or defaults.
* @param {SettingsType} settings - Object containing settings from the database.
*/ */
async initializeState() { initializeState(settings: Settings | undefined) {
await db.open();
const settings: Settings | undefined =
await db.settings.get(MASTER_SETTINGS_KEY);
this.activeDid = (settings?.activeDid as string) || ""; this.activeDid = (settings?.activeDid as string) || "";
this.apiServer = (settings?.apiServer as string) || ""; this.apiServer = (settings?.apiServer as string) || "";
this.apiServerInput = (settings?.apiServer as string) || ""; this.apiServerInput = (settings?.apiServer as string) || "";
@@ -823,16 +752,10 @@ export default class AccountViewView extends Vue {
(settings?.firstName || "") + (settings?.firstName || "") +
(settings?.lastName ? ` ${settings.lastName}` : ""); // pre v 0.1.3 (settings?.lastName ? ` ${settings.lastName}` : ""); // pre v 0.1.3
this.isRegistered = !!settings?.isRegistered; this.isRegistered = !!settings?.isRegistered;
this.imageServer = (settings?.imageServer as string) || "";
this.profileImageUrl = settings?.profileImageUrl as string; this.profileImageUrl = settings?.profileImageUrl as string;
this.showContactGives = !!settings?.showContactGivesInline; this.showContactGives = !!settings?.showContactGivesInline;
this.hideRegisterPromptOnNewContact = this.hideRegisterPromptOnNewContact =
!!settings?.hideRegisterPromptOnNewContact; !!settings?.hideRegisterPromptOnNewContact;
this.passkeyExpirationMinutes =
(settings?.passkeyExpirationMinutes as number) ??
DEFAULT_PASSKEY_EXPIRATION_MINUTES;
this.previousPasskeyExpirationMinutes = this.passkeyExpirationMinutes;
this.showGeneralAdvanced = !!settings?.showGeneralAdvanced;
this.showShortcutBvc = !!settings?.showShortcutBvc; this.showShortcutBvc = !!settings?.showShortcutBvc;
this.warnIfProdServer = !!settings?.warnIfProdServer; this.warnIfProdServer = !!settings?.warnIfProdServer;
this.warnIfTestServer = !!settings?.warnIfTestServer; this.warnIfTestServer = !!settings?.warnIfTestServer;
@@ -840,6 +763,49 @@ export default class AccountViewView extends Vue {
this.webPushServerInput = (settings?.webPushServer as string) || ""; this.webPushServerInput = (settings?.webPushServer as string) || "";
} }
public async getIdentity(activeDid: string): Promise<IIdentifier | null> {
try {
// Open the accounts database
await accountsDB.open();
// Search for the account with the matching DID (decentralized identifier)
const account: { identity?: string } | undefined =
await accountsDB.accounts.where("did").equals(activeDid).first();
// Return parsed identity or null if not found
return JSON.parse((account?.identity as string) || "null");
} catch (error) {
console.error("Failed to find account:", error);
return null;
}
}
/**
* Asynchronously retrieves headers for HTTP requests.
*
* @param {IIdentifier} identity - The identity object for which to generate the headers.
* @returns {Promise<Record<string,string>>} A Promise that resolves to an object containing the headers.
*
* @throws Will throw an error if unable to generate an access token.
*/
public async getHeaders(
identity: IIdentifier,
): Promise<Record<string, string>> {
try {
const token = await accessToken(identity);
const headers: Record<string, string> = {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
};
return headers;
} catch (error) {
console.error("Failed to get headers:", error);
return Promise.reject(error);
}
}
// call fn, copy text to the clipboard, then redo fn after 2 seconds // call fn, copy text to the clipboard, then redo fn after 2 seconds
doCopyTwoSecRedo(text: string, fn: () => void) { doCopyTwoSecRedo(text: string, fn: () => void) {
fn(); fn();
@@ -853,11 +819,6 @@ export default class AccountViewView extends Vue {
this.updateShowContactAmounts(); this.updateShowContactAmounts();
} }
toggleShowGeneralAdvanced() {
this.showGeneralAdvanced = !this.showGeneralAdvanced;
this.updateShowGeneralAdvanced();
}
toggleProdWarning() { toggleProdWarning() {
this.warnIfProdServer = !this.warnIfProdServer; this.warnIfProdServer = !this.warnIfProdServer;
this.updateWarnIfProdServer(this.warnIfProdServer); this.updateWarnIfProdServer(this.warnIfProdServer);
@@ -879,19 +840,25 @@ export default class AccountViewView extends Vue {
/** /**
* Processes the identity and updates the component's state. * Processes the identity and updates the component's state.
* @param {IdentityType} identity - Object containing identity information.
*/ */
async processIdentity() { processIdentity(identity: IIdentifier) {
const account: Account | undefined = await getAccount(this.activeDid); if (
if (account?.identity) { identity &&
const identity = JSON.parse(account.identity as string) as IIdentifier; identity.keys &&
identity.keys.length > 0 &&
identity.keys[0].meta
) {
this.publicHex = identity.keys[0].publicKeyHex; this.publicHex = identity.keys[0].publicKeyHex;
this.publicBase64 = Buffer.from(this.publicHex, "hex").toString("base64"); this.publicBase64 = Buffer.from(this.publicHex, "hex").toString("base64");
this.derivationPath = identity.keys[0].meta?.derivationPath as string; this.derivationPath = identity.keys[0].meta?.derivationPath as string;
await this.checkLimitsFor(this.activeDid);
} else if (account?.publicKeyHex) { db.settings.update(MASTER_SETTINGS_KEY, {
this.publicHex = account.publicKeyHex as string; activeDid: identity.did,
this.publicBase64 = Buffer.from(this.publicHex, "hex").toString("base64"); });
await this.checkLimitsFor(this.activeDid); this.checkLimitsFor(identity);
} else {
// Handle the case where any of these are null or undefined
} }
} }
@@ -920,24 +887,58 @@ export default class AccountViewView extends Vue {
this.notificationMaybeChanged = true; this.notificationMaybeChanged = true;
} }
public async updateShowContactAmounts() { /**
await db.open(); * Handles errors and updates the component's state accordingly.
await db.settings.update(MASTER_SETTINGS_KEY, { * @param {Error} err - The error object.
showContactGivesInline: this.showContactGives, */
}); handleError(err: unknown) {
if (
err instanceof Error &&
err.message ===
"Attempted to load account records with no identifier available."
) {
this.limitsMessage = "No identifier.";
} else {
console.error("Telling user to clear cache at page create because:", err);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Loading Account",
text: "Clear your cache and start over (after data backup).",
},
-1,
);
}
} }
public async updateShowGeneralAdvanced() { public async updateShowContactAmounts() {
try {
await db.open(); await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, { db.settings.update(MASTER_SETTINGS_KEY, {
showGeneralAdvanced: this.showGeneralAdvanced, showContactGivesInline: this.showContactGives,
}); });
} catch (err) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Updating Contact Setting",
text: "The setting may not have saved. Try again, maybe after restarting the app.",
},
-1,
);
console.error(
"Telling user to try again after contact-amounts setting update because:",
err,
);
}
} }
public async updateWarnIfProdServer(newSetting: boolean) { public async updateWarnIfProdServer(newSetting: boolean) {
try { try {
await db.open(); await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, { db.settings.update(MASTER_SETTINGS_KEY, {
warnIfProdServer: newSetting, warnIfProdServer: newSetting,
}); });
} catch (err) { } catch (err) {
@@ -958,35 +959,71 @@ export default class AccountViewView extends Vue {
} }
public async updateWarnIfTestServer(newSetting: boolean) { public async updateWarnIfTestServer(newSetting: boolean) {
try {
await db.open(); await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, { db.settings.update(MASTER_SETTINGS_KEY, {
warnIfTestServer: newSetting, warnIfTestServer: newSetting,
}); });
} catch (err) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Updating Test Warning",
text: "The setting may not have saved. Try again, maybe after restarting the app.",
},
-1,
);
console.error(
"Telling user to try again after test-server-warning setting update because:",
err,
);
}
} }
public async toggleHideRegisterPromptOnNewContact() { public async toggleHideRegisterPromptOnNewContact() {
const newSetting = !this.hideRegisterPromptOnNewContact; const newSetting = !this.hideRegisterPromptOnNewContact;
try {
await db.open(); await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, { db.settings.update(MASTER_SETTINGS_KEY, {
hideRegisterPromptOnNewContact: newSetting, hideRegisterPromptOnNewContact: newSetting,
}); });
this.hideRegisterPromptOnNewContact = newSetting; this.hideRegisterPromptOnNewContact = newSetting;
} catch (err) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Updating Setting",
text: "The setting may not have saved. Try again, maybe after restarting the app.",
},
-1,
);
console.error("Telling user to try again because:", err);
} }
public async updatePasskeyExpiration() {
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, {
passkeyExpirationMinutes: this.passkeyExpirationMinutes,
});
clearPasskeyToken();
this.passkeyExpirationDescription = tokenExpiryTimeDescription();
} }
public async updateShowShortcutBvc(newSetting: boolean) { public async updateShowShortcutBvc(newSetting: boolean) {
try {
await db.open(); await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, { db.settings.update(MASTER_SETTINGS_KEY, {
showShortcutBvc: newSetting, showShortcutBvc: newSetting,
}); });
} catch (err) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Updating BVC Shortcut Setting",
text: "The setting may not have saved. Try again, maybe after restarting the app.",
},
-1,
);
console.error(
"Telling user to try again after BVC-shortcut setting update because:",
err,
);
}
} }
/** /**
@@ -1150,8 +1187,9 @@ export default class AccountViewView extends Vue {
} }
async checkLimits() { async checkLimits() {
if (this.activeDid) { const identity = await this.getIdentity(this.activeDid);
this.checkLimitsFor(this.activeDid); if (identity) {
this.checkLimitsFor(identity);
} else { } else {
this.limitsMessage = this.limitsMessage =
"You have no identifier, or your data has been corrupted."; "You have no identifier, or your data has been corrupted.";
@@ -1163,7 +1201,7 @@ export default class AccountViewView extends Vue {
* *
* Updates component state variables `limits`, `limitsMessage`, and `loadingLimits`. * Updates component state variables `limits`, `limitsMessage`, and `loadingLimits`.
*/ */
public async checkLimitsFor(did: string) { public async checkLimitsFor(identity: IIdentifier) {
this.loadingLimits = true; this.loadingLimits = true;
this.limitsMessage = ""; this.limitsMessage = "";
@@ -1171,7 +1209,7 @@ export default class AccountViewView extends Vue {
const resp = await fetchEndorserRateLimits( const resp = await fetchEndorserRateLimits(
this.apiServer, this.apiServer,
this.axios, this.axios,
did, identity,
); );
if (resp.status === 200) { if (resp.status === 200) {
this.endorserLimits = resp.data; this.endorserLimits = resp.data;
@@ -1179,7 +1217,7 @@ export default class AccountViewView extends Vue {
// the user was not known to be registered, but now they are (because we got no error) so let's record it // the user was not known to be registered, but now they are (because we got no error) so let's record it
try { try {
await db.open(); await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, { db.settings.update(MASTER_SETTINGS_KEY, {
isRegistered: true, isRegistered: true,
}); });
this.isRegistered = true; this.isRegistered = true;
@@ -1196,7 +1234,11 @@ export default class AccountViewView extends Vue {
); );
} }
} }
const imageResp = await fetchImageRateLimits(this.axios, did); const imageResp = await fetchImageRateLimits(
this.apiServer,
this.axios,
identity,
);
if (imageResp.status === 200) { if (imageResp.status === 200) {
this.imageLimits = imageResp.data; this.imageLimits = imageResp.data;
} }
@@ -1206,7 +1248,7 @@ export default class AccountViewView extends Vue {
try { try {
await db.open(); await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, { db.settings.update(MASTER_SETTINGS_KEY, {
isRegistered: false, isRegistered: false,
}); });
this.isRegistered = false; this.isRegistered = false;
@@ -1231,8 +1273,8 @@ export default class AccountViewView extends Vue {
(data?.error?.message as string) || "Bad server response."; (data?.error?.message as string) || "Bad server response.";
console.error( console.error(
"Got bad response retrieving limits, which usually means user isn't registered.", "Got bad response retrieving limits, which usually means user isn't registered.",
error,
); );
//console.error(error);
} else { } else {
this.limitsMessage = "Got an error retrieving limits."; this.limitsMessage = "Got an error retrieving limits.";
console.error("Got some error retrieving limits:", error); console.error("Got some error retrieving limits:", error);
@@ -1293,9 +1335,9 @@ export default class AccountViewView extends Vue {
* *
* @param {AccountType} account - The account object. * @param {AccountType} account - The account object.
*/ */
private updateActiveAccountProperties(account: Account) { private updateActiveAccountProperties(account: IAccount) {
this.activeDid = account.did; this.activeDid = account.did;
this.derivationPath = account.derivationPath || ""; this.derivationPath = account.derivationPath;
this.publicHex = account.publicKeyHex; this.publicHex = account.publicKeyHex;
this.publicBase64 = Buffer.from(this.publicHex, "hex").toString("base64"); this.publicBase64 = Buffer.from(this.publicHex, "hex").toString("base64");
} }
@@ -1309,7 +1351,7 @@ export default class AccountViewView extends Vue {
async onClickSaveApiServer() { async onClickSaveApiServer() {
await db.open(); await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, { db.settings.update(MASTER_SETTINGS_KEY, {
apiServer: this.apiServerInput, apiServer: this.apiServerInput,
}); });
this.apiServer = this.apiServerInput; this.apiServer = this.apiServerInput;
@@ -1317,7 +1359,7 @@ export default class AccountViewView extends Vue {
async onClickSavePushServer() { async onClickSavePushServer() {
await db.open(); await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, { db.settings.update(MASTER_SETTINGS_KEY, {
webPushServer: this.webPushServerInput, webPushServer: this.webPushServerInput,
}); });
this.webPushServer = this.webPushServerInput; this.webPushServer = this.webPushServerInput;
@@ -1336,7 +1378,7 @@ export default class AccountViewView extends Vue {
(this.$refs.imageMethodDialog as ImageMethodDialog).open( (this.$refs.imageMethodDialog as ImageMethodDialog).open(
async (imgUrl) => { async (imgUrl) => {
await db.open(); await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, { db.settings.update(MASTER_SETTINGS_KEY, {
profileImageUrl: imgUrl, profileImageUrl: imgUrl,
}); });
this.profileImageUrl = imgUrl; this.profileImageUrl = imgUrl;
@@ -1366,13 +1408,20 @@ export default class AccountViewView extends Vue {
return; return;
} }
try { try {
const headers = await getHeaders(this.activeDid); const identity = await this.getIdentity(this.activeDid);
this.passkeyExpirationDescription = tokenExpiryTimeDescription(); if (!identity) {
throw Error("No identity found.");
}
const token = await accessToken(identity);
const response = await this.axios.delete( const response = await this.axios.delete(
DEFAULT_IMAGE_API_SERVER + DEFAULT_IMAGE_API_SERVER +
"/image/" + "/image/" +
encodeURIComponent(this.profileImageUrl), encodeURIComponent(this.profileImageUrl),
{ headers }, {
headers: {
Authorization: `Bearer ${token}`,
},
},
); );
if (response.status === 204) { if (response.status === 204) {
// don't bother with a notification // don't bother with a notification
@@ -1392,7 +1441,7 @@ export default class AccountViewView extends Vue {
} }
await db.open(); await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, { db.settings.update(MASTER_SETTINGS_KEY, {
profileImageUrl: undefined, profileImageUrl: undefined,
}); });
@@ -1404,7 +1453,7 @@ export default class AccountViewView extends Vue {
console.error("The image was already deleted:", error); console.error("The image was already deleted:", error);
await db.open(); await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, { db.settings.update(MASTER_SETTINGS_KEY, {
profileImageUrl: undefined, profileImageUrl: undefined,
}); });

View File

@@ -1,101 +0,0 @@
<template>
<QuickNav />
<!-- CONTENT -->
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Breadcrumb -->
<div id="ViewBreadcrumb" class="mb-8">
<h1 class="text-lg text-center font-light relative px-7">
<!-- Back -->
<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" />
</button>
Raw Claim
</h1>
</div>
<div class="flex">
<textarea rows="20" class="border-2 w-full" v-model="claimStr"></textarea>
</div>
<button
class="block w-full text-center text-lg font-bold uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md"
@click="submitClaim()"
>
Sign &amp; Send
</button>
</section>
</template>
<script lang="ts">
import { IIdentifier } from "@veramo/core";
import { Component, Vue } from "vue-facing-decorator";
import GiftedDialog from "@/components/GiftedDialog.vue";
import { NotificationIface } from "@/constants/app";
import { accountsDB, db } from "@/db/index";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import * as serverUtil from "@/libs/endorserServer";
import QuickNav from "@/components/QuickNav.vue";
import { Account } from "@/db/tables/accounts";
@Component({
components: { GiftedDialog, QuickNav },
})
export default class ClaimAddRawView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
accountIdentityStr: string = "null";
activeDid = "";
apiServer = "";
claimStr = "";
async mounted() {
await db.open();
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
this.activeDid = settings?.activeDid || "";
this.apiServer = settings?.apiServer || "";
this.claimStr = this.$route.query.claim;
try {
this.veriClaim = JSON.parse(this.claimStr);
this.claimStr = JSON.stringify(this.veriClaim, null, 2);
} catch (e) {
// ignore a parse
}
}
async submitClaim() {
const fullClaim = JSON.parse(this.claimStr);
const result = await serverUtil.createAndSubmitClaim(
fullClaim,
this.activeDid,
this.apiServer,
this.axios,
);
if (result.type === "success") {
this.$notify(
{
group: "alert",
type: "success",
title: "Success",
text: "Claim submitted.",
},
5000,
);
} else {
console.error("Got error submitting the claim:", result);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "There was a problem submitting the claim. See logs for more info.",
},
-1,
);
}
}
}
</script>

View File

@@ -407,7 +407,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { AxiosError } from "axios"; import { AxiosError, RawAxiosRequestHeaders } from "axios";
import * as yaml from "js-yaml"; import * as yaml from "js-yaml";
import * as R from "ramda"; import * as R from "ramda";
import { IIdentifier } from "@veramo/core"; import { IIdentifier } from "@veramo/core";
@@ -419,6 +419,7 @@ import { NotificationIface } from "@/constants/app";
import { accountsDB, db } from "@/db/index"; import { accountsDB, db } from "@/db/index";
import { Contact } from "@/db/tables/contacts"; import { Contact } from "@/db/tables/contacts";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";
import * as serverUtil from "@/libs/endorserServer"; import * as serverUtil from "@/libs/endorserServer";
import * as libsUtil from "@/libs/util"; import * as libsUtil from "@/libs/util";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
@@ -431,6 +432,7 @@ import { GiverReceiverInputInfo } from "@/libs/endorserServer";
export default class ClaimView extends Vue { export default class ClaimView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void; $notify!: (notification: NotificationIface, timeout?: number) => void;
accountIdentityStr: string = "null";
activeDid = ""; activeDid = "";
allMyDids: Array<string> = []; allMyDids: Array<string> = [];
allContacts: Array<Contact> = []; allContacts: Array<Contact> = [];
@@ -483,12 +485,15 @@ export default class ClaimView extends Vue {
const accounts = accountsDB.accounts; const accounts = accountsDB.accounts;
const accountsArr: Array<Account> = await accounts?.toArray(); const accountsArr: Array<Account> = await accounts?.toArray();
this.allMyDids = accountsArr.map((acc) => acc.did); this.allMyDids = accountsArr.map((acc) => acc.did);
const account = accountsArr.find((acc) => acc.did === this.activeDid);
this.accountIdentityStr = (account?.identity as string) || "null";
const identity = JSON.parse(this.accountIdentityStr);
const pathParam = window.location.pathname.substring("/claim/".length); const pathParam = window.location.pathname.substring("/claim/".length);
let claimId; let claimId;
if (pathParam) { if (pathParam) {
claimId = decodeURIComponent(pathParam); claimId = decodeURIComponent(pathParam);
await this.loadClaim(claimId, this.activeDid); await this.loadClaim(claimId, identity);
} else { } else {
this.$notify( this.$notify(
{ {
@@ -522,6 +527,33 @@ export default class ClaimView extends Vue {
); );
} }
public async getIdentity(activeDid: string): Promise<IIdentifier> {
await accountsDB.open();
const account = (await accountsDB.accounts
.where("did")
.equals(activeDid)
.first()) as Account;
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error(
"Attempted to load project records with no identifier available.",
);
}
return identity;
}
public async getHeaders(identity: IIdentifier) {
const headers: RawAxiosRequestHeaders = {
"Content-Type": "application/json",
};
if (identity) {
const token = await accessToken(identity);
headers["Authorization"] = "Bearer " + token;
}
return headers;
}
// Isn't there a better way to make this available to the template? // Isn't there a better way to make this available to the template?
didInfo(did: string) { didInfo(did: string) {
return serverUtil.didInfo( return serverUtil.didInfo(
@@ -532,12 +564,12 @@ export default class ClaimView extends Vue {
); );
} }
async loadClaim(claimId: string, userDid: string) { async loadClaim(claimId: string, identity: IIdentifier) {
const urlPath = libsUtil.isGlobalUri(claimId) const urlPath = libsUtil.isGlobalUri(claimId)
? "/api/claim/byHandle/" ? "/api/claim/byHandle/"
: "/api/claim/"; : "/api/claim/";
const url = this.apiServer + urlPath + encodeURIComponent(claimId); const url = this.apiServer + urlPath + encodeURIComponent(claimId);
const headers = await serverUtil.getHeaders(userDid); const headers = await this.getHeaders(identity);
try { try {
const resp = await this.axios.get(url, { headers }); const resp = await this.axios.get(url, { headers });
@@ -569,7 +601,7 @@ export default class ClaimView extends Vue {
this.apiServer + this.apiServer +
"/api/v2/report/gives?handleId=" + "/api/v2/report/gives?handleId=" +
encodeURIComponent(this.veriClaim.handleId as string); encodeURIComponent(this.veriClaim.handleId as string);
const giveHeaders = await serverUtil.getHeaders(userDid); const giveHeaders = await this.getHeaders(identity);
const giveResp = await this.axios.get(giveUrl, { const giveResp = await this.axios.get(giveUrl, {
headers: giveHeaders, headers: giveHeaders,
}); });
@@ -583,7 +615,7 @@ export default class ClaimView extends Vue {
this.apiServer + this.apiServer +
"/api/v2/report/offers?handleId=" + "/api/v2/report/offers?handleId=" +
encodeURIComponent(this.veriClaim.handleId as string); encodeURIComponent(this.veriClaim.handleId as string);
const offerHeaders = await serverUtil.getHeaders(userDid); const offerHeaders = await this.getHeaders(identity);
const offerResp = await this.axios.get(offerUrl, { const offerResp = await this.axios.get(offerUrl, {
headers: offerHeaders, headers: offerHeaders,
}); });
@@ -599,14 +631,12 @@ export default class ClaimView extends Vue {
this.apiServer + this.apiServer +
"/api/report/issuersWhoClaimedOrConfirmed?claimId=" + "/api/report/issuersWhoClaimedOrConfirmed?claimId=" +
encodeURIComponent(serverUtil.stripEndorserPrefix(claimId)); encodeURIComponent(serverUtil.stripEndorserPrefix(claimId));
const confirmHeaders = await serverUtil.getHeaders(userDid); const confirmHeaders = await this.getHeaders(identity);
const response = await this.axios.get(confirmUrl, { const response = await this.axios.get(confirmUrl, {
headers: confirmHeaders, headers: confirmHeaders,
}); });
if (response.status === 200) { if (response.status === 200) {
const resultList1 = response.data.result || []; const resultList1 = response.data.result || [];
//const publicUrls = resultList.publicUrls || [];
delete resultList1.publicUrls;
const resultList2 = R.reject(serverUtil.isHiddenDid, resultList1); const resultList2 = R.reject(serverUtil.isHiddenDid, resultList1);
const resultList3 = R.reject( const resultList3 = R.reject(
(did: string) => did === this.veriClaim.issuer, (did: string) => did === this.veriClaim.issuer,
@@ -641,9 +671,15 @@ export default class ClaimView extends Vue {
} }
async showFullClaim(claimId: string) { async showFullClaim(claimId: string) {
await accountsDB.open();
const accounts = accountsDB.accounts;
const accountsArr: Account[] = await accounts?.toArray();
const account = accountsArr.find((acc) => acc.did === this.activeDid);
const identity = JSON.parse((account?.identity as string) || "null");
const url = const url =
this.apiServer + "/api/claim/full/" + encodeURIComponent(claimId); this.apiServer + "/api/claim/full/" + encodeURIComponent(claimId);
const headers = await serverUtil.getHeaders(this.activeDid); const headers = await this.getHeaders(identity);
try { try {
const resp = await this.axios.get(url, { headers }); const resp = await this.axios.get(url, { headers });
@@ -722,7 +758,7 @@ export default class ClaimView extends Vue {
}; };
const result = await serverUtil.createAndSubmitClaim( const result = await serverUtil.createAndSubmitClaim(
confirmationClaim, confirmationClaim,
this.activeDid, await this.getIdentity(this.activeDid),
this.apiServer, this.apiServer,
this.axios, this.axios,
); );
@@ -756,7 +792,7 @@ export default class ClaimView extends Vue {
}; };
this.$router.push(route).then(async () => { this.$router.push(route).then(async () => {
this.resetThisValues(); this.resetThisValues();
await this.loadClaim(claimId, this.activeDid); await this.loadClaim(claimId, JSON.parse(this.accountIdentityStr));
}); });
} }

View File

@@ -27,7 +27,7 @@
</h1> </h1>
</div> </div>
<div v-if="giveDetails && !isLoading"> <div v-if="giveDetails">
<div class="flex justify-center"> <div class="flex justify-center">
<button <button
class="col-span-1 bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md" class="col-span-1 bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md"
@@ -371,9 +371,9 @@
> >
</div> </div>
</div> </div>
<div v-else-if="!isLoading">This does not have details to confirm.</div> <div v-else>This does not have details to confirm.</div>
<div class="mt-4" v-if="!isLoading"> <div class="mt-4">
<a <a
@click="showClaimPage(veriClaim.id)" @click="showClaimPage(veriClaim.id)"
class="text-blue-500 cursor-pointer" class="text-blue-500 cursor-pointer"
@@ -382,18 +382,11 @@
All Generic Info All Generic Info
</a> </a>
</div> </div>
<div
class="fixed left-6 bottom-24 text-center text-4xl leading-none bg-slate-400 text-white w-14 py-2.5 rounded-full"
v-if="isLoading"
>
<fa icon="spinner" class="fa-spin-pulse"></fa>
</div>
</section> </section>
</template> </template>
<script lang="ts"> <script lang="ts">
import { AxiosError } from "axios"; import { AxiosError, RawAxiosRequestHeaders } from "axios";
import * as yaml from "js-yaml"; import * as yaml from "js-yaml";
import * as R from "ramda"; import * as R from "ramda";
import { IIdentifier } from "@veramo/core"; import { IIdentifier } from "@veramo/core";
@@ -407,6 +400,7 @@ import { accountsDB, db } from "@/db/index";
import { Account } from "@/db/tables/accounts"; import { Account } from "@/db/tables/accounts";
import { Contact } from "@/db/tables/contacts"; import { Contact } from "@/db/tables/contacts";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";
import * as serverUtil from "@/libs/endorserServer"; import * as serverUtil from "@/libs/endorserServer";
import { displayAmount, GiverReceiverInputInfo } from "@/libs/endorserServer"; import { displayAmount, GiverReceiverInputInfo } from "@/libs/endorserServer";
import * as libsUtil from "@/libs/util"; import * as libsUtil from "@/libs/util";
@@ -419,6 +413,7 @@ import { isGiveAction } from "@/libs/util";
export default class ClaimView extends Vue { export default class ClaimView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void; $notify!: (notification: NotificationIface, timeout?: number) => void;
accountIdentityStr: string = "null";
activeDid = ""; activeDid = "";
allMyDids: Array<string> = []; allMyDids: Array<string> = [];
allContacts: Array<Contact> = []; allContacts: Array<Contact> = [];
@@ -431,7 +426,6 @@ export default class ClaimView extends Vue {
giveDetails = null; giveDetails = null;
giverName = ""; giverName = "";
issuerName = ""; issuerName = "";
isLoading = false;
numConfsNotVisible = 0; // number of hidden DIDs in the confirmerIdList, minus the issuer if they aren't visible numConfsNotVisible = 0; // number of hidden DIDs in the confirmerIdList, minus the issuer if they aren't visible
recipientName = ""; recipientName = "";
showDetails = false; showDetails = false;
@@ -458,7 +452,6 @@ export default class ClaimView extends Vue {
} }
async mounted() { async mounted() {
this.isLoading = true;
await db.open(); await db.open();
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings; const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
this.activeDid = settings?.activeDid || ""; this.activeDid = settings?.activeDid || "";
@@ -469,6 +462,9 @@ export default class ClaimView extends Vue {
const accounts = accountsDB.accounts; const accounts = accountsDB.accounts;
const accountsArr: Array<Account> = await accounts?.toArray(); const accountsArr: Array<Account> = await accounts?.toArray();
this.allMyDids = accountsArr.map((acc) => acc.did); this.allMyDids = accountsArr.map((acc) => acc.did);
const account = accountsArr.find((acc) => acc.did === this.activeDid);
this.accountIdentityStr = (account?.identity as string) || "null";
const identity = JSON.parse(this.accountIdentityStr);
const pathParam = window.location.pathname.substring( const pathParam = window.location.pathname.substring(
"/confirm-gift/".length, "/confirm-gift/".length,
@@ -476,7 +472,7 @@ export default class ClaimView extends Vue {
let claimId; let claimId;
if (pathParam) { if (pathParam) {
claimId = decodeURIComponent(pathParam); claimId = decodeURIComponent(pathParam);
await this.loadClaim(claimId, this.activeDid); await this.loadClaim(claimId, identity);
} else { } else {
this.$notify( this.$notify(
{ {
@@ -492,8 +488,6 @@ export default class ClaimView extends Vue {
// When Chrome compatibility is fixed https://developer.mozilla.org/en-US/docs/Web/API/Web_Share_API#api.navigator.canshare // When Chrome compatibility is fixed https://developer.mozilla.org/en-US/docs/Web/API/Web_Share_API#api.navigator.canshare
// then use this truer check: navigator.canShare && navigator.canShare() // then use this truer check: navigator.canShare && navigator.canShare()
this.canShare = !!navigator.share; this.canShare = !!navigator.share;
this.isLoading = false;
} }
// insert a space before any capital letters except the initial letter // insert a space before any capital letters except the initial letter
@@ -525,6 +519,33 @@ export default class ClaimView extends Vue {
); );
} }
public async getIdentity(activeDid: string): Promise<IIdentifier> {
await accountsDB.open();
const account = (await accountsDB.accounts
.where("did")
.equals(activeDid)
.first()) as Account;
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error(
"Attempted to load project records with no identifier available.",
);
}
return identity;
}
public async getHeaders(identity: IIdentifier) {
const headers: RawAxiosRequestHeaders = {
"Content-Type": "application/json",
};
if (identity) {
const token = await accessToken(identity);
headers["Authorization"] = "Bearer " + token;
}
return headers;
}
// Isn't there a better way to make this available to the template? // Isn't there a better way to make this available to the template?
didInfo(did: string | undefined) { didInfo(did: string | undefined) {
return serverUtil.didInfo( return serverUtil.didInfo(
@@ -535,14 +556,14 @@ export default class ClaimView extends Vue {
); );
} }
async loadClaim(claimId: string, userDid: string) { async loadClaim(claimId: string, identity: IIdentifier) {
const urlPath = libsUtil.isGlobalUri(claimId) const urlPath = libsUtil.isGlobalUri(claimId)
? "/api/claim/byHandle/" ? "/api/claim/byHandle/"
: "/api/claim/"; : "/api/claim/";
const url = this.apiServer + urlPath + encodeURIComponent(claimId); const url = this.apiServer + urlPath + encodeURIComponent(claimId);
try { try {
const headers = await serverUtil.getHeaders(userDid); const headers = await this.getHeaders(identity);
const resp = await this.axios.get(url, { headers }); const resp = await this.axios.get(url, { headers });
// resp.data is: // resp.data is:
// - a Jwt from https://api.endorser.ch/api-docs/ // - a Jwt from https://api.endorser.ch/api-docs/
@@ -582,7 +603,7 @@ export default class ClaimView extends Vue {
this.apiServer + this.apiServer +
"/api/v2/report/gives?handleId=" + "/api/v2/report/gives?handleId=" +
encodeURIComponent(this.veriClaim.handleId as string); encodeURIComponent(this.veriClaim.handleId as string);
const giveHeaders = await serverUtil.getHeaders(userDid); const giveHeaders = await this.getHeaders(identity);
const giveResp = await this.axios.get(giveUrl, { const giveResp = await this.axios.get(giveUrl, {
headers: giveHeaders, headers: giveHeaders,
}); });
@@ -653,14 +674,12 @@ export default class ClaimView extends Vue {
this.apiServer + this.apiServer +
"/api/report/issuersWhoClaimedOrConfirmed?claimId=" + "/api/report/issuersWhoClaimedOrConfirmed?claimId=" +
encodeURIComponent(serverUtil.stripEndorserPrefix(claimId)); encodeURIComponent(serverUtil.stripEndorserPrefix(claimId));
const confirmHeaders = await serverUtil.getHeaders(userDid); const confirmHeaders = await this.getHeaders(identity);
const response = await this.axios.get(confirmUrl, { const response = await this.axios.get(confirmUrl, {
headers: confirmHeaders, headers: confirmHeaders,
}); });
if (response.status === 200) { if (response.status === 200) {
const resultList1 = response.data.result || []; const resultList1 = response.data.result || [];
//const publicUrls = resultList.publicUrls || [];
delete resultList1.publicUrls;
const resultList2 = R.reject(serverUtil.isHiddenDid, resultList1); const resultList2 = R.reject(serverUtil.isHiddenDid, resultList1);
const resultList3 = R.reject( const resultList3 = R.reject(
(did: string) => did === this.giveDetails.agentDid, (did: string) => did === this.giveDetails.agentDid,
@@ -728,7 +747,7 @@ export default class ClaimView extends Vue {
}; };
const result = await serverUtil.createAndSubmitClaim( const result = await serverUtil.createAndSubmitClaim(
confirmationClaim, confirmationClaim,
this.activeDid, await this.getIdentity(this.activeDid),
this.apiServer, this.apiServer,
this.axios, this.axios,
); );
@@ -762,7 +781,7 @@ export default class ClaimView extends Vue {
}; };
this.$router.push(route).then(async () => { this.$router.push(route).then(async () => {
this.resetThisValues(); this.resetThisValues();
await this.loadClaim(claimId, this.activeDid); await this.loadClaim(claimId, JSON.parse(this.accountIdentityStr));
}); });
} }

View File

@@ -55,7 +55,7 @@
{{ new Date(record.issuedAt).toLocaleString() }} {{ new Date(record.issuedAt).toLocaleString() }}
</td> </td>
<td class="p-1"> <td class="p-1">
<span v-if="record.agentDid == contact?.did"> <span v-if="record.agentDid == contact.did">
<div class="font-bold"> <div class="font-bold">
{{ displayAmount(record.unit, record.amount) }} {{ displayAmount(record.unit, record.amount) }}
<span v-if="record.amountConfirmed" title="Confirmed"> <span v-if="record.amountConfirmed" title="Confirmed">
@@ -71,7 +71,7 @@
</span> </span>
</td> </td>
<td class="p-1"> <td class="p-1">
<span v-if="record.agentDid == contact?.did"> <span v-if="record.agentDid == contact.did">
<fa icon="arrow-left" class="text-slate-400 fa-fw" /> <fa icon="arrow-left" class="text-slate-400 fa-fw" />
</span> </span>
<span v-else> <span v-else>
@@ -79,7 +79,7 @@
</span> </span>
</td> </td>
<td class="p-1"> <td class="p-1">
<span v-if="record.agentDid != contact?.did"> <span v-if="record.agentDid != contact.did">
<div class="font-bold"> <div class="font-bold">
{{ displayAmount(record.unit, record.amount) }} {{ displayAmount(record.unit, record.amount) }}
<span v-if="record.amountConfirmed" title="Confirmed"> <span v-if="record.amountConfirmed" title="Confirmed">
@@ -105,8 +105,10 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { AxiosError, AxiosRequestHeaders } from "axios"; import { AxiosError } from "axios";
import * as didJwt from "did-jwt";
import * as R from "ramda"; import * as R from "ramda";
import { IIdentifier } from "@veramo/core";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
@@ -114,11 +116,10 @@ import { NotificationIface } from "@/constants/app";
import { accountsDB, db } from "@/db/index"; import { accountsDB, db } from "@/db/index";
import { Contact } from "@/db/tables/contacts"; import { Contact } from "@/db/tables/contacts";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken, SimpleSigner } from "@/libs/crypto";
import { import {
AgreeVerifiableCredential, AgreeVerifiableCredential,
createEndorserJwtVcFromClaim,
displayAmount, displayAmount,
getHeaders,
GiveSummaryRecord, GiveSummaryRecord,
GiveVerifiableCredential, GiveVerifiableCredential,
SCHEMA_ORG_CONTEXT, SCHEMA_ORG_CONTEXT,
@@ -141,6 +142,31 @@ export default class ContactAmountssView extends Vue {
this.numAccounts = await accountsDB.accounts.count(); this.numAccounts = await accountsDB.accounts.count();
} }
public async getIdentity(activeDid: string) {
await accountsDB.open();
const account = await accountsDB.accounts
.where("did")
.equals(activeDid)
.first();
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error(
"Attempted to load Give records with no identifier available.",
);
}
return identity;
}
public async getHeaders(identity: IIdentifier) {
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
return headers;
}
async created() { async created() {
try { try {
await db.open(); await db.open();
@@ -148,8 +174,8 @@ export default class ContactAmountssView extends Vue {
this.contact = (await db.contacts.get(contactDid)) || null; this.contact = (await db.contacts.get(contactDid)) || null;
const settings = await db.settings.get(MASTER_SETTINGS_KEY); const settings = await db.settings.get(MASTER_SETTINGS_KEY);
this.activeDid = (settings?.activeDid as string) || ""; this.activeDid = settings?.activeDid || "";
this.apiServer = (settings?.apiServer as string) || ""; this.apiServer = settings?.apiServer || "";
if (this.activeDid && this.contact) { if (this.activeDid && this.contact) {
this.loadGives(this.activeDid, this.contact); this.loadGives(this.activeDid, this.contact);
@@ -173,14 +199,15 @@ export default class ContactAmountssView extends Vue {
async loadGives(activeDid: string, contact: Contact) { async loadGives(activeDid: string, contact: Contact) {
try { try {
const identity = await this.getIdentity(this.activeDid);
let result: Array<GiveSummaryRecord> = []; let result: Array<GiveSummaryRecord> = [];
const url = const url =
this.apiServer + this.apiServer +
"/api/v2/report/gives?agentDid=" + "/api/v2/report/gives?agentDid=" +
encodeURIComponent(this.activeDid) + encodeURIComponent(identity.did) +
"&recipientDid=" + "&recipientDid=" +
encodeURIComponent(contact.did); encodeURIComponent(contact.did);
const headers = await getHeaders(activeDid); const headers = await this.getHeaders(identity);
const resp = await this.axios.get(url, { headers }); const resp = await this.axios.get(url, { headers });
if (resp.status === 200) { if (resp.status === 200) {
result = resp.data.data; result = resp.data.data;
@@ -206,8 +233,8 @@ export default class ContactAmountssView extends Vue {
"/api/v2/report/gives?agentDid=" + "/api/v2/report/gives?agentDid=" +
encodeURIComponent(contact.did) + encodeURIComponent(contact.did) +
"&recipientDid=" + "&recipientDid=" +
encodeURIComponent(this.activeDid); encodeURIComponent(identity.did);
const headers2 = await getHeaders(activeDid); const headers2 = await this.getHeaders(identity);
const resp2 = await this.axios.get(url2, { headers: headers2 }); const resp2 = await this.axios.get(url2, { headers: headers2 });
if (resp2.status === 200) { if (resp2.status === 200) {
result = R.concat(result, resp2.data.data); result = R.concat(result, resp2.data.data);
@@ -262,21 +289,42 @@ export default class ContactAmountssView extends Vue {
object: origClaim, object: origClaim,
}; };
const vcJwt: string = await createEndorserJwtVcFromClaim( // Make a payload for the claim
this.activeDid, const vcPayload = {
vcClaim, vc: {
); "@context": ["https://www.w3.org/2018/credentials/v1"],
type: ["VerifiableCredential"],
credentialSubject: vcClaim,
},
};
// Create a signature using private key of identity
const identity = await this.getIdentity(this.activeDid);
if (identity.keys[0].privateKeyHex !== null) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
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 // Make the xhr request payload
const payload = JSON.stringify({ jwtEncoded: vcJwt }); const payload = JSON.stringify({ jwtEncoded: vcJwt });
const url = this.apiServer + "/api/v2/claim"; const url = this.apiServer + "/api/v2/claim";
const headers = getHeaders(this.activeDid) as AxiosRequestHeaders; const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
try { try {
const resp = await this.axios.post(url, payload, { headers }); const resp = await this.axios.post(url, payload, { headers });
if (resp.data?.success) { if (resp.data?.success) {
record.amountConfirmed = record.amountConfirmed = origClaim.object?.amountOfThisGood || 1;
(origClaim.object?.amountOfThisGood as number) || 1;
} }
} catch (error) { } catch (error) {
let userMessage = "There was an error. See logs for more info."; let userMessage = "There was an error. See logs for more info.";
@@ -302,6 +350,7 @@ export default class ContactAmountssView extends Vue {
); );
} }
} }
}
cannotConfirmMessage() { cannotConfirmMessage() {
this.$notify( this.$notify(

View File

@@ -72,15 +72,17 @@
<script lang="ts"> <script lang="ts">
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { IIdentifier } from "@veramo/core";
import GiftedDialog from "@/components/GiftedDialog.vue"; import GiftedDialog from "@/components/GiftedDialog.vue";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
import EntityIcon from "@/components/EntityIcon.vue"; import EntityIcon from "@/components/EntityIcon.vue";
import { NotificationIface } from "@/constants/app"; import { NotificationIface } from "@/constants/app";
import { db, accountsDB } from "@/db/index"; import { db, accountsDB } from "@/db/index";
import { AccountsSchema } from "@/db/tables/accounts"; import { Account, AccountsSchema } from "@/db/tables/accounts";
import { Contact } from "@/db/tables/contacts"; import { Contact } from "@/db/tables/contacts";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";
import { GiverReceiverInputInfo } from "@/libs/endorserServer"; import { GiverReceiverInputInfo } from "@/libs/endorserServer";
@Component({ @Component({
@@ -132,7 +134,32 @@ export default class ContactGiftingView extends Vue {
} }
} }
openDialog(giver?: GiverReceiverInputInfo) { public async getIdentity(activeDid: string) {
await accountsDB.open();
const account = (await accountsDB.accounts
.where("did")
.equals(activeDid)
.first()) as Account;
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error(
"Attempted to load Give records with no identifier available.",
);
}
return identity;
}
public async getHeaders(identity: IIdentifier) {
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
return headers;
}
openDialog(giver: GiverReceiverInputInfo) {
const recipient = this.projectId const recipient = this.projectId
? undefined ? undefined
: { did: this.activeDid, name: "you" }; : { did: this.activeDid, name: "you" };

View File

@@ -24,7 +24,6 @@
> >
<span class="text-red">Beware!</span> <span class="text-red">Beware!</span>
You aren't sharing your name, so quickly You aren't sharing your name, so quickly
<br />
<router-link <router-link
:to="{ name: 'new-edit-account' }" :to="{ name: 'new-edit-account' }"
class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-1 rounded-md" class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-1 rounded-md"
@@ -34,11 +33,7 @@
</p> </p>
</div> </div>
<div <div @click="onCopyToClipboard()" v-if="activeDid" class="text-center">
@click="onCopyUrlToClipboard()"
v-if="activeDid && activeDid.startsWith(ETHR_DID_PREFIX)"
class="text-center"
>
<!-- <!--
Play with display options: https://qr-code-styling.com/ Play with display options: https://qr-code-styling.com/
See docs: https://www.npmjs.com/package/qr-code-generator-vue3 See docs: https://www.npmjs.com/package/qr-code-generator-vue3
@@ -49,18 +44,8 @@
:dotsOptions="{ type: 'square' }" :dotsOptions="{ type: 'square' }"
class="flex justify-center" class="flex justify-center"
/> />
<span> <span> Click that QR to copy your contact URL to your clipboard. </span>
Click this or QR code to copy your contact URL to your clipboard. <div>Not scanning? Show it in pieces.</div>
</span>
</div>
<div v-else-if="activeDid" class="text-center">
<!-- Not an ETHR DID so force them to paste it. (Passkey Peer DIDs are too big.) -->
<span @click="onCopyDidToClipboard()" class="text-blue-500">
Click here to copy your DID to your clipboard.
</span>
<span>
Then give it to them so they can paste it in their list of People.
</span>
</div> </div>
<div class="text-center" v-else> <div class="text-center" v-else>
You have no identitifiers yet, so You have no identitifiers yet, so
@@ -87,7 +72,7 @@
<script lang="ts"> <script lang="ts">
import { AxiosError } from "axios"; import { AxiosError } from "axios";
import { Buffer } from "buffer/"; import * as didJwt from "did-jwt";
import { sha256 } from "ethereum-cryptography/sha256.js"; import { sha256 } from "ethereum-cryptography/sha256.js";
import QRCodeVue3 from "qr-code-generator-vue3"; import QRCodeVue3 from "qr-code-generator-vue3";
import * as R from "ramda"; import * as R from "ramda";
@@ -98,22 +83,24 @@ import { useClipboard } from "@vueuse/core";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
import { NotificationIface } from "@/constants/app"; import { NotificationIface } from "@/constants/app";
import { accountsDB, db } from "@/db/index"; import { accountsDB, db } from "@/db/index";
import { Account } from "@/db/tables/accounts";
import { Contact } from "@/db/tables/contacts"; import { Contact } from "@/db/tables/contacts";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { import {
deriveAddress, deriveAddress,
getContactPayloadFromJwtUrl, getContactPayloadFromJwtUrl,
nextDerivationPath, nextDerivationPath,
SimpleSigner,
} from "@/libs/crypto"; } from "@/libs/crypto";
import { import {
CONTACT_URL_PREFIX, CONTACT_URL_PREFIX,
createEndorserJwtForDid,
ENDORSER_JWT_URL_LOCATION, ENDORSER_JWT_URL_LOCATION,
isDid, isDid,
register, register,
setVisibilityUtil, setVisibilityUtil,
} from "@/libs/endorserServer"; } from "@/libs/endorserServer";
import { ETHR_DID_PREFIX } from "@/libs/crypto/vc";
import { Buffer } from "buffer/";
@Component({ @Component({
components: { components: {
@@ -132,8 +119,6 @@ export default class ContactQRScanShow extends Vue {
isRegistered = false; isRegistered = false;
qrValue = ""; qrValue = "";
ETHR_DID_PREFIX = ETHR_DID_PREFIX;
async created() { async created() {
await db.open(); await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY); const settings = await db.settings.get(MASTER_SETTINGS_KEY);
@@ -148,9 +133,17 @@ export default class ContactQRScanShow extends Vue {
const accounts = await accountsDB.accounts.toArray(); const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === this.activeDid, accounts); const account = R.find((acc) => acc.did === this.activeDid, accounts);
if (account) { if (account) {
const publicKeyHex = account.publicKeyHex; const identity = await this.getIdentity(this.activeDid);
const publicKeyHex = identity.keys[0].publicKeyHex;
const publicEncKey = Buffer.from(publicKeyHex, "hex").toString("base64"); const publicEncKey = Buffer.from(publicKeyHex, "hex").toString("base64");
const newDerivPath = nextDerivationPath(account.derivationPath);
const nextPublicHex = deriveAddress(account.mnemonic, newDerivPath)[2];
const nextPublicEncKey = Buffer.from(nextPublicHex, "hex");
const nextPublicEncKeyHash = sha256(nextPublicEncKey);
const nextPublicEncKeyHashBase64 =
Buffer.from(nextPublicEncKeyHash).toString("base64");
const contactInfo = { const contactInfo = {
iat: Date.now(), iat: Date.now(),
iss: this.activeDid, iss: this.activeDid,
@@ -159,28 +152,21 @@ export default class ContactQRScanShow extends Vue {
(settings?.firstName || "") + (settings?.firstName || "") +
(settings?.lastName ? ` ${settings.lastName}` : ""), // deprecated, pre v 0.1.3 (settings?.lastName ? ` ${settings.lastName}` : ""), // deprecated, pre v 0.1.3
publicEncKey, publicEncKey,
nextPublicEncKeyHash: nextPublicEncKeyHashBase64,
profileImageUrl: settings?.profileImageUrl, profileImageUrl: settings?.profileImageUrl,
registered: settings?.isRegistered, registered: settings?.isRegistered,
}, },
}; };
if (account?.mnemonic && account?.derivationPath) { const alg = undefined;
const newDerivPath = nextDerivationPath( const privateKeyHex: string = identity.keys[0].privateKeyHex;
account.derivationPath as string, const signer = await SimpleSigner(privateKeyHex);
); // create a JWT for the request
const nextPublicHex = deriveAddress( const vcJwt: string = await didJwt.createJWT(contactInfo, {
account.mnemonic as string, alg: alg,
newDerivPath, issuer: identity.did,
)[2]; signer: signer,
const nextPublicEncKey = Buffer.from(nextPublicHex, "hex"); });
const nextPublicEncKeyHash = sha256(nextPublicEncKey);
const nextPublicEncKeyHashBase64 =
Buffer.from(nextPublicEncKeyHash).toString("base64");
contactInfo.own.nextPublicEncKeyHash = nextPublicEncKeyHashBase64;
}
const vcJwt = await createEndorserJwtForDid(this.activeDid, contactInfo);
const viewPrefix = CONTACT_URL_PREFIX + ENDORSER_JWT_URL_LOCATION; const viewPrefix = CONTACT_URL_PREFIX + ENDORSER_JWT_URL_LOCATION;
this.qrValue = viewPrefix + vcJwt; this.qrValue = viewPrefix + vcJwt;
} }
@@ -198,6 +184,23 @@ export default class ContactQRScanShow extends Vue {
); );
} }
public async getIdentity(activeDid: string) {
await accountsDB.open();
const accounts = await accountsDB.accounts.toArray();
const account: Account | undefined = R.find(
(acc) => acc.did === activeDid,
accounts,
);
const identity = JSON.parse((account?.identity as string) || "null");
if (!identity) {
throw new Error(
"Attempted to show contact info with no identifier available.",
);
}
return identity;
}
/** /**
* *
* @param content is the result of a QR scan, an array with one item with a rawValue property * @param content is the result of a QR scan, an array with one item with a rawValue property
@@ -430,7 +433,7 @@ export default class ContactQRScanShow extends Vue {
); );
} }
onCopyUrlToClipboard() { onCopyToClipboard() {
//this.onScanDetect([{ rawValue: this.qrValue }]); // good for testing //this.onScanDetect([{ rawValue: this.qrValue }]); // good for testing
useClipboard() useClipboard()
.copy(this.qrValue) .copy(this.qrValue)
@@ -447,22 +450,5 @@ export default class ContactQRScanShow extends Vue {
); );
}); });
} }
onCopyDidToClipboard() {
//this.onScanDetect([{ rawValue: this.qrValue }]); // good for testing
useClipboard()
.copy(this.activeDid)
.then(() => {
this.$notify(
{
group: "alert",
type: "info",
title: "Copied",
text: "Your DID was copied to the clipboard. Have them paste it on their 'People' screen to add you.",
},
10000,
);
});
}
} }
</script> </script>

View File

@@ -303,20 +303,20 @@
import { AxiosError } from "axios"; import { AxiosError } from "axios";
import { IndexableType } from "dexie"; import { IndexableType } from "dexie";
import * as R from "ramda"; import * as R from "ramda";
import { IIdentifier } from "@veramo/core";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { Router } from "vue-router"; import { Router } from "vue-router";
import { AppString, NotificationIface } from "@/constants/app"; import { AppString, NotificationIface } from "@/constants/app";
import { db } from "@/db/index"; import { accountsDB, db } from "@/db/index";
import { Contact } from "@/db/tables/contacts"; import { Contact } from "@/db/tables/contacts";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { getContactPayloadFromJwtUrl } from "@/libs/crypto"; import { accessToken, getContactPayloadFromJwtUrl } from "@/libs/crypto";
import { import {
CONTACT_CSV_HEADER, CONTACT_CSV_HEADER,
CONTACT_URL_PREFIX, CONTACT_URL_PREFIX,
GiverReceiverInputInfo, GiverReceiverInputInfo,
GiveSummaryRecord, GiveSummaryRecord,
getHeaders,
isDid, isDid,
register, register,
setVisibilityUtil, setVisibilityUtil,
@@ -326,6 +326,7 @@ import QuickNav from "@/components/QuickNav.vue";
import EntityIcon from "@/components/EntityIcon.vue"; import EntityIcon from "@/components/EntityIcon.vue";
import GiftedDialog from "@/components/GiftedDialog.vue"; import GiftedDialog from "@/components/GiftedDialog.vue";
import OfferDialog from "@/components/OfferDialog.vue"; import OfferDialog from "@/components/OfferDialog.vue";
import { Account } from "@/db/tables/accounts";
import { Buffer } from "buffer/"; import { Buffer } from "buffer/";
@@ -399,6 +400,36 @@ export default class ContactsView extends Vue {
); );
} }
public async getIdentity(activeDid: string): Promise<IIdentifier> {
await accountsDB.open();
const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === activeDid, accounts) as Account;
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error(
"Attempted to load Give records with no identifier available.",
);
}
return identity;
}
public async getHeaders(identity: IIdentifier) {
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
return headers;
}
public async getHeadersAndIdentity(activeDid: string) {
const identity = await this.getIdentity(activeDid);
const headers = await this.getHeaders(identity);
return { headers, identity };
}
async loadGives() { async loadGives() {
if (!this.activeDid) { if (!this.activeDid) {
return; return;
@@ -450,7 +481,7 @@ export default class ContactsView extends Vue {
}; };
try { try {
const headers = await getHeaders(this.activeDid); const { headers } = await this.getHeadersAndIdentity(this.activeDid);
const givenByUrl = const givenByUrl =
this.apiServer + this.apiServer +
"/api/v2/report/gives?agentDid=" + "/api/v2/report/gives?agentDid=" +
@@ -923,19 +954,8 @@ export default class ContactsView extends Vue {
this.apiServer + this.apiServer +
"/api/report/canDidExplicitlySeeMe?did=" + "/api/report/canDidExplicitlySeeMe?did=" +
encodeURIComponent(contact.did); encodeURIComponent(contact.did);
const headers = await getHeaders(this.activeDid); const identity = await this.getIdentity(this.activeDid);
if (!headers["Authorization"]) { const headers = await this.getHeaders(identity);
this.$notify(
{
group: "alert",
type: "danger",
title: "No Identity",
text: "There is no identity to use to check visibility.",
},
3000,
);
return;
}
try { try {
const resp = await this.axios.get(url, { headers }); const resp = await this.axios.get(url, { headers });

View File

@@ -136,11 +136,11 @@ import { NotificationIface } from "@/constants/app";
import { accountsDB, db } from "@/db/index"; import { accountsDB, db } from "@/db/index";
import { Contact } from "@/db/tables/contacts"; import { Contact } from "@/db/tables/contacts";
import { BoundingBox, MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { BoundingBox, MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";
import { import {
capitalizeAndInsertSpacesBeforeCaps, capitalizeAndInsertSpacesBeforeCaps,
didInfoForContact, didInfoForContact,
displayAmount, displayAmount,
getHeaders,
GenericCredWrapper, GenericCredWrapper,
GenericVerifiableCredential, GenericVerifiableCredential,
GiveVerifiableCredential, GiveVerifiableCredential,
@@ -203,6 +203,30 @@ export default class DIDView extends Vue {
this.allMyDids = allAccounts.map((acc) => acc.did); this.allMyDids = allAccounts.map((acc) => acc.did);
} }
public async buildHeaders(): Promise<HeadersInit> {
const headers: HeadersInit = {
"Content-Type": "application/json",
};
if (this.activeDid) {
await accountsDB.open();
const allAccounts = await accountsDB.accounts.toArray();
const account = allAccounts.find((acc) => acc.did === this.activeDid);
const identity = JSON.parse((account?.identity as string) || "null");
if (!identity) {
throw new Error(
"An ID is chosen but there are no keys for it so it cannot be used to talk with the service. Switch your ID.",
);
}
headers["Authorization"] = "Bearer " + (await accessToken(identity));
} else {
// it's OK without auth... we just won't get any identifiers
}
return headers;
}
/** /**
* Data loader used by infinite scroller * Data loader used by infinite scroller
* @param payload is the flag from the InfiniteScroll indicating if it should load * @param payload is the flag from the InfiniteScroll indicating if it should load
@@ -231,7 +255,7 @@ export default class DIDView extends Vue {
this.apiServer + "/api/v2/report/claims?" + queryParams + postfix, this.apiServer + "/api/v2/report/claims?" + queryParams + postfix,
{ {
method: "GET", method: "GET",
headers: await getHeaders(this.activeDid), headers: await this.buildHeaders(),
}, },
); );

View File

@@ -138,7 +138,8 @@ import { NotificationIface } from "@/constants/app";
import { accountsDB, db } from "@/db/index"; import { accountsDB, db } from "@/db/index";
import { Contact } from "@/db/tables/contacts"; import { Contact } from "@/db/tables/contacts";
import { BoundingBox, MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { BoundingBox, MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { didInfo, getHeaders, PlanData } from "@/libs/endorserServer"; import { accessToken } from "@/libs/crypto";
import { didInfo, PlanData } from "@/libs/endorserServer";
@Component({ @Component({
components: { components: {
@@ -202,6 +203,30 @@ export default class DiscoverView extends Vue {
} }
} }
public async buildHeaders(): Promise<HeadersInit> {
const headers: HeadersInit = {
"Content-Type": "application/json",
};
if (this.activeDid) {
await accountsDB.open();
const allAccounts = await accountsDB.accounts.toArray();
const account = allAccounts.find((acc) => acc.did === this.activeDid);
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error(
"An ID is chosen but there are no keys for it so it cannot be used to talk with the service. Switch your ID.",
);
}
headers["Authorization"] = "Bearer " + (await accessToken(identity));
} else {
// it's OK without auth... we just won't get any identifiers
}
return headers;
}
public async searchAll(beforeId?: string) { public async searchAll(beforeId?: string) {
this.resetCounts(); this.resetCounts();
@@ -222,7 +247,7 @@ export default class DiscoverView extends Vue {
this.apiServer + "/api/v2/report/plans?" + queryParams, this.apiServer + "/api/v2/report/plans?" + queryParams,
{ {
method: "GET", method: "GET",
headers: await getHeaders(this.activeDid), headers: await this.buildHeaders(),
}, },
); );
@@ -312,7 +337,7 @@ export default class DiscoverView extends Vue {
this.apiServer + "/api/v2/report/plansByLocation?" + queryParams, this.apiServer + "/api/v2/report/plansByLocation?" + queryParams,
{ {
method: "GET", method: "GET",
headers: await getHeaders(this.activeDid), headers: await this.buildHeaders(),
}, },
); );

View File

@@ -21,17 +21,8 @@
<h1 class="text-4xl text-center font-light px-4 mb-4">What Was Given</h1> <h1 class="text-4xl text-center font-light px-4 mb-4">What Was Given</h1>
<h1 class="text-xl font-bold text-center mb-4"> <h1 class="text-xl font-bold text-center mb-4">
<span>From {{ giverName }}</span> <span>From {{ giverName || "somebody not named" }}</span>
<span> <span> to {{ recipientName || "somebody not named" }}</span>
to
{{
givenToProject
? projectName
: givenToRecipient
? recipientName
: "someone unidentified"
}}</span
>
</h1> </h1>
<textarea <textarea
class="block w-full rounded border border-slate-400 mb-2 px-3 py-2" class="block w-full rounded border border-slate-400 mb-2 px-3 py-2"
@@ -87,7 +78,7 @@
<div class="h-7 mt-4 flex"> <div class="h-7 mt-4 flex">
<input <input
v-if="projectId && !givenToRecipient" v-if="projectId && !givenToUser"
type="checkbox" type="checkbox"
class="h-6 w-6 mr-2" class="h-6 w-6 mr-2"
v-model="givenToProject" v-model="givenToProject"
@@ -109,24 +100,20 @@
<div class="h-7 mt-4 flex"> <div class="h-7 mt-4 flex">
<input <input
v-if="recipientDid && !givenToProject" v-if="!givenToProject"
type="checkbox" type="checkbox"
class="h-6 w-6 mr-2" class="h-6 w-6 mr-2"
v-model="givenToRecipient" v-model="givenToUser"
/> />
<fa <fa
v-else v-else
icon="square" icon="square"
class="bg-slate-500 text-slate-500 h-5 w-5 px-0.5 py-0.5 mr-2 rounded" class="bg-slate-500 text-slate-500 h-5 w-5 px-0.5 py-0.5 mr-2 rounded"
@click="notifyUserOfRecipient()" @click="
notifyUser('You cannot assign this both a project and also to you.')
"
/> />
<label class="text-sm mt-1"> <label class="text-sm mt-1">This was given to you</label>
{{
recipientDid
? "This was given to " + recipientName
: "No recipient was chosen."
}}
</label>
</div> </div>
<div class="mt-4 flex"> <div class="mt-4 flex">
@@ -134,20 +121,6 @@
<label class="text-sm mt-1">This was a trade (not a gift)</label> <label class="text-sm mt-1">This was a trade (not a gift)</label>
</div> </div>
<div class="mt-4 flex">
<router-link
:to="{
name: 'claim-add-raw',
query: {
claim: constructGiveParam(),
},
}"
class="text-blue-500"
>
Edit & Submit Raw
</router-link>
</div>
<p class="text-center mb-2 mt-6 italic"> <p class="text-center mb-2 mt-6 italic">
Sign & Send to publish to the world Sign & Send to publish to the world
<fa <fa
@@ -180,17 +153,11 @@ import ImageMethodDialog from "@/components/ImageMethodDialog.vue";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
import TopMessage from "@/components/TopMessage.vue"; import TopMessage from "@/components/TopMessage.vue";
import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "@/constants/app"; import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "@/constants/app";
import { accountsDB, db } from "@/db/index"; import { db } from "@/db/index";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { import { createAndSubmitGive, getPlanFromCache } from "@/libs/endorserServer";
constructGive,
createAndSubmitGive,
didInfo,
getHeaders,
getPlanFromCache,
} from "@/libs/endorserServer";
import * as libsUtil from "@/libs/util"; import * as libsUtil from "@/libs/util";
import { Contact } from "@/db/tables/contacts"; import { accessToken } from "@/libs/crypto";
@Component({ @Component({
components: { components: {
@@ -209,7 +176,7 @@ export default class GiftedDetails extends Vue {
description = ""; description = "";
destinationNameAfter = ""; destinationNameAfter = "";
givenToProject = false; givenToProject = false;
givenToRecipient = false; givenToUser = false;
giverDid: string | undefined; giverDid: string | undefined;
giverName = ""; giverName = "";
hideBackButton = false; hideBackButton = false;
@@ -221,6 +188,7 @@ export default class GiftedDetails extends Vue {
projectName = "a project"; projectName = "a project";
recipientDid = ""; recipientDid = "";
recipientName = ""; recipientName = "";
showGivenToUser = false;
unitCode = "HUR"; unitCode = "HUR";
libsUtil = libsUtil; libsUtil = libsUtil;
@@ -266,36 +234,18 @@ export default class GiftedDetails extends Vue {
this.apiServer = settings?.apiServer || ""; this.apiServer = settings?.apiServer || "";
this.activeDid = settings?.activeDid || ""; this.activeDid = settings?.activeDid || "";
let allContacts: Contact[] = [];
let allMyDids: string[] = [];
if (
(this.giverDid && !this.giverName) ||
(this.recipientDid && !this.recipientName)
) {
allContacts = await db.contacts.toArray();
await accountsDB.open();
const allAccounts = await accountsDB.accounts.toArray();
allMyDids = allAccounts.map((acc) => acc.did);
if (this.giverDid && !this.giverName) { if (this.giverDid && !this.giverName) {
this.giverName = didInfo( this.giverName =
this.giverDid, this.giverDid === this.activeDid ? "you" : "someone not named";
this.activeDid,
allMyDids,
allContacts,
);
} }
this.givenToUser = this.recipientDid === this.activeDid;
if (this.recipientDid && !this.recipientName) { if (this.recipientDid && !this.recipientName) {
this.recipientName = didInfo( this.recipientName =
this.recipientDid, this.recipientDid === this.activeDid ? "you" : "someone not named";
this.activeDid,
allMyDids,
allContacts,
);
}
} }
this.givenToProject = !!this.projectId; this.givenToProject = !!this.projectId;
this.givenToRecipient = !this.givenToProject && !!this.recipientDid; this.givenToUser =
!this.projectId && this.recipientDid === this.activeDid;
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) { } catch (err: any) {
@@ -313,12 +263,14 @@ export default class GiftedDetails extends Vue {
if (this.projectId) { if (this.projectId) {
// console.log("Getting project name from cache", this.projectId); // console.log("Getting project name from cache", this.projectId);
const identity = await libsUtil.getIdentity(this.activeDid);
const project = await getPlanFromCache( const project = await getPlanFromCache(
this.projectId, this.projectId,
identity,
this.axios, this.axios,
this.apiServer, this.apiServer,
this.activeDid,
); );
console.log("Got project name from cache", project);
this.projectName = project?.name this.projectName = project?.name
? "the project: " + project.name ? "the project: " + project.name
: "a project"; : "a project";
@@ -380,12 +332,17 @@ export default class GiftedDetails extends Vue {
return; return;
} }
try { try {
const headers = await getHeaders(this.activeDid); const identity = await libsUtil.getIdentity(this.activeDid);
const token = await accessToken(identity);
const response = await this.axios.delete( const response = await this.axios.delete(
DEFAULT_IMAGE_API_SERVER + DEFAULT_IMAGE_API_SERVER +
"/image/" + "/image/" +
encodeURIComponent(this.imageUrl), encodeURIComponent(this.imageUrl),
{ headers }, {
headers: {
Authorization: `Bearer ${token}`,
},
},
); );
if (response.status === 204) { if (response.status === 204) {
// don't bother with a notification // don't bother with a notification
@@ -485,6 +442,18 @@ export default class GiftedDetails extends Vue {
await this.recordGive(); await this.recordGive();
} }
notifyUser(message: string) {
this.$notify(
{
group: "alert",
type: "warning",
title: "Error",
text: message,
},
3000,
);
}
notifyUserOfProject() { notifyUserOfProject() {
if (!this.projectId) { if (!this.projectId) {
this.$notify( this.$notify(
@@ -497,38 +466,13 @@ export default class GiftedDetails extends Vue {
3000, 3000,
); );
} else { } else {
// must be because givenToRecipient is true // must be because givenToUser is true
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
type: "warning", type: "warning",
title: "Error", title: "Error",
text: "You cannot assign both to a project and to a recipient.", text: "You cannot assign both to a project and to yourself.",
},
3000,
);
}
}
notifyUserOfRecipient() {
if (!this.recipientDid) {
this.$notify(
{
group: "alert",
type: "warning",
title: "Error",
text: "To assign to a recipient, you must open this dialog from a contact.",
},
3000,
);
} else {
// must be because givenToProject is true
this.$notify(
{
group: "alert",
type: "warning",
title: "Error",
text: "You cannot assign both to a recipient and to a project.",
}, },
3000, 3000,
); );
@@ -544,14 +488,18 @@ export default class GiftedDetails extends Vue {
*/ */
public async recordGive() { public async recordGive() {
try { try {
const recipientDid = this.givenToRecipient const identity = await libsUtil.getIdentity(this.activeDid);
? this.recipientDid const recipientDid =
: undefined; this.recipientDid === this.activeDid
? this.givenToUser
? this.activeDid
: undefined
: this.recipientDid;
const projectId = this.givenToProject ? this.projectId : undefined; const projectId = this.givenToProject ? this.projectId : undefined;
const result = await createAndSubmitGive( const result = await createAndSubmitGive(
this.axios, this.axios,
this.apiServer, this.apiServer,
this.activeDid, identity,
this.giverDid, this.giverDid,
recipientDid, recipientDid,
this.description, this.description,
@@ -614,24 +562,6 @@ export default class GiftedDetails extends Vue {
} }
} }
constructGiveParam() {
const recipientDid = this.givenToRecipient ? this.recipientDid : undefined;
const projectId = this.givenToProject ? this.projectId : undefined;
const giveClaim = constructGive(
this.giverDid,
recipientDid,
this.description,
parseFloat(this.amountInput),
this.unitCode,
projectId,
this.offerId,
this.isTrade,
this.imageUrl,
);
const claimStr = JSON.stringify(giveClaim);
return claimStr;
}
// Helper functions for readability // Helper functions for readability
/** /**

View File

@@ -24,15 +24,16 @@
<!-- eslint-disable prettier/prettier --> <!-- eslint-disable prettier/prettier -->
<div> <div>
<p> <p>
This app focuses on gifts & gratitude, using them to build cool things with your network. This app is a window into data that you and your friends own, focused on
gifts and collaboration.
</p> </p>
<h2 class="text-xl font-semibold">What is the idea here?</h2> <h2 class="text-xl font-semibold">What is the idea here?</h2>
<p> <p>
We are building networks of people who want to grow a giving society. We are building networks of people who want to grow a giving society.
First of all, let's build gratitude: see what people have given, and recognize First of all, you can see what people have given, and also recognize
gifts you've seen. This is done in a way that leaves a permanent record -- one that gifts you've seen, in a way that leaves a permanent record -- one that
came from you, and that the recipient can prove it was for them. This is came from you, and the recipient can prove it was for them. This is
personally gratifying, but it extends to broader work: volunteers get personally gratifying, but it extends to broader work: volunteers get
confirmation of activity, and selectively show off their contributions confirmation of activity, and selectively show off their contributions
and network. and network.
@@ -75,6 +76,9 @@
<p> <p>
Go Go
<router-link class="text-blue-500" to="/import-account">import your identifier</router-link>. <router-link class="text-blue-500" to="/import-account">import your identifier</router-link>.
If you don't want the old one, click "Advanced" and check the box to erase it.
(The erase option only shows if you have exactly one identifier.
For more in-depth surgery, you'll have to erase data from the browser or reinstall.)
</p> </p>
<h2 class="text-xl font-semibold">How do I add someone else?</h2> <h2 class="text-xl font-semibold">How do I add someone else?</h2>
@@ -82,8 +86,8 @@
<a href="/help-onboarding" target="_blank" class="text-blue-500"> <a href="/help-onboarding" target="_blank" class="text-blue-500">
Use these instructions. Use these instructions.
</a> </a>
To start scanning, go to the To start scanning, go
<router-link class="text-blue-500" to="/contact-qr">contact-scanning page.</router-link> <router-link class="text-blue-500" to="/contact-qr">here.</router-link>
</p> </p>
<p> <p>
If they are not nearby to scan QR codes, you each can tap on the QR code If they are not nearby to scan QR codes, you each can tap on the QR code
@@ -115,7 +119,7 @@
</ul> </ul>
<h2 class="text-xl font-semibold"> <h2 class="text-xl font-semibold">
How do I backup my other private text data like settings & contacts? How do I backup my non-secret, non-public text data?
</h2> </h2>
<ul class="list-disc list-outside ml-4"> <ul class="list-disc list-outside ml-4">
<li> <li>
@@ -129,7 +133,7 @@
</ul> </ul>
<h2 class="text-xl font-semibold"> <h2 class="text-xl font-semibold">
How do I backup my profile image? How do I backup my non-secret, non-public image?
</h2> </h2>
<ul class="list-disc list-outside ml-4"> <ul class="list-disc list-outside ml-4">
<li> <li>
@@ -139,7 +143,7 @@
</ul> </ul>
<h2 class="text-xl font-semibold"> <h2 class="text-xl font-semibold">
How do I backup other data I've posted? How do I backup my public data?
</h2> </h2>
<ul class="list-disc list-outside ml-4"> <ul class="list-disc list-outside ml-4">
<li> <li>
@@ -176,7 +180,6 @@
<li> <li>
Go to Your Identity <fa icon="circle-user" class="fa-fw" /> page, Go to Your Identity <fa icon="circle-user" class="fa-fw" /> page,
click Advanced, and follow the instructions for the Contacts & Settings Database "Import". click Advanced, and follow the instructions for the Contacts & Settings Database "Import".
Beware that this will erase your existing contact & settings.
</li> </li>
</ul> </ul>
</div> </div>
@@ -337,7 +340,7 @@
<h2 class="text-xl font-semibold">What are the terms & conditions and the privacy policy?</h2> <h2 class="text-xl font-semibold">What are the terms & conditions and the privacy policy?</h2>
<p style="display:inline; align-items: center"> <p style="display:inline; align-items: center">
This work is public domain. If you like rules, reference This work is public domain, governed by
<a href="http://creativecommons.org/publicdomain/zero/1.0?ref=chooser-v1" target="_blank" rel="license noopener noreferrer"> <a href="http://creativecommons.org/publicdomain/zero/1.0?ref=chooser-v1" target="_blank" rel="license noopener noreferrer">
<span class="text-blue-500 mr-1">CC0 1.0</span> <span class="text-blue-500 mr-1">CC0 1.0</span>
<img <img
@@ -363,26 +366,6 @@
</a> </a>
</p> </p>
<h2 class="text-xl font-semibold">How can I contribute?</h2>
<p>
If you have skills, contact us below.
If you have Bitcoin, donate to
<button
@click="
doCopyTwoSecRedo(
'bc1q90v4ted6cpt63tjfh2lvd5xzfc67sd4g9w8xma',
() => (showDidCopy = !showDidCopy)
)
"
class="text-blue-500 ml-2"
>
bc1q90v4ted6cpt63tjfh2lvd5xzfc67sd4g9w8xma
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
</button>
<span v-show="showDidCopy">Copied</span>
For other donations, contact us.
</p>
<h2 class="text-xl font-semibold">Where can I read more?</h2> <h2 class="text-xl font-semibold">Where can I read more?</h2>
<p> <p>
This is part of the This is part of the
@@ -396,7 +379,7 @@
<p>{{ package.version }} ({{ commitHash }})</p> <p>{{ package.version }} ({{ commitHash }})</p>
<h2 class="text-xl font-semibold"> <h2 class="text-xl font-semibold">
For any other questions, like getting a new account or removing all your data from the public ledger: For any other questions, including removing all your data from the public ledger:
</h2> </h2>
<p> <p>
Contact us at Contact us at
@@ -411,7 +394,6 @@
<script lang="ts"> <script lang="ts">
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { useClipboard } from "@vueuse/core";
import * as Package from "../../package.json"; import * as Package from "../../package.json";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
@@ -423,14 +405,5 @@ export default class Help extends Vue {
package = Package; package = Package;
commitHash = import.meta.env.VITE_GIT_HASH; commitHash = import.meta.env.VITE_GIT_HASH;
showDidCopy = false;
// call fn, copy text to the clipboard, then redo fn after 2 seconds
doCopyTwoSecRedo(text: string, fn: () => void) {
fn();
useClipboard()
.copy(text)
.then(() => setTimeout(fn, 2000));
}
} }
</script> </script>

View File

@@ -5,7 +5,7 @@
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-2 pb-24 max-w-3xl mx-auto"> <section id="Content" class="p-2 pb-24 max-w-3xl mx-auto">
<h1 id="ViewHeading" class="text-4xl text-center font-light px-4 mb-8"> <h1 id="ViewHeading" class="text-4xl text-center font-light px-4 mb-8">
{{ AppString.APP_NAME }} Time Safari
</h1> </h1>
<!-- prompt to install notifications --> <!-- prompt to install notifications -->
@@ -77,28 +77,35 @@
<div v-else> <div v-else>
<!-- !isCreatingIdentifier --> <!-- !isCreatingIdentifier -->
<!-- They should have an identifier, even if it's an auto-generated one that they'll never use. -->
<div class="mb-4">
<div <div
v-if="!isRegistered" v-if="!activeDid"
class="bg-amber-200 rounded-md overflow-hidden text-center px-4 py-3 mb-4"
>
<p class="text-lg mb-3">
Want to connect with your contacts, or share contributions or
projects?
</p>
<router-link
:to="{ name: 'start' }"
class="block text-center text-md font-bold bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mt-2 px-2 py-3 rounded-md"
>
Create An Identifier
</router-link>
</div>
<div
v-else-if="!isRegistered"
class="bg-amber-200 rounded-md overflow-hidden text-center px-4 py-3 mb-4" class="bg-amber-200 rounded-md overflow-hidden text-center px-4 py-3 mb-4"
> >
<!-- activeDid && !isRegistered --> <!-- activeDid && !isRegistered -->
To share, someone must register you. Someone must register you before you can give kudos or make offers or
create projects... basically before doing anything.
<router-link <router-link
:to="{ name: 'contact-qr' }" :to="{ name: 'contact-qr' }"
class="block text-center text-md font-bold bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mt-2 px-2 py-3 rounded-md" class="block text-center text-md font-bold bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mt-2 px-2 py-3 rounded-md"
> >
Show Them Default Identifier Info Show Them Your Identifier Info
</router-link> </router-link>
<div v-if="PASSKEYS_ENABLED" class="flex justify-end w-full">
<router-link
:to="{ name: 'start' }"
class="block text-right text-md font-bold bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mt-2 px-2 py-3 rounded-md"
>
See all your options first
</router-link>
</div>
</div> </div>
<div v-else> <div v-else>
@@ -159,7 +166,6 @@
</div> </div>
</div> </div>
</div> </div>
</div>
<GiftedDialog ref="customDialog" /> <GiftedDialog ref="customDialog" />
<GiftedPrompts ref="giftedPrompts" /> <GiftedPrompts ref="giftedPrompts" />
@@ -299,10 +305,10 @@
<script lang="ts"> <script lang="ts">
import { UAParser } from "ua-parser-js"; import { UAParser } from "ua-parser-js";
import { IIdentifier } from "@veramo/core";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { Router } from "vue-router"; import { Router } from "vue-router";
import App from "../App.vue";
import EntityIcon from "@/components/EntityIcon.vue"; import EntityIcon from "@/components/EntityIcon.vue";
import GiftedDialog from "@/components/GiftedDialog.vue"; import GiftedDialog from "@/components/GiftedDialog.vue";
import GiftedPrompts from "@/components/GiftedPrompts.vue"; import GiftedPrompts from "@/components/GiftedPrompts.vue";
@@ -310,12 +316,9 @@ import FeedFilters from "@/components/FeedFilters.vue";
import InfiniteScroll from "@/components/InfiniteScroll.vue"; import InfiniteScroll from "@/components/InfiniteScroll.vue";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
import TopMessage from "@/components/TopMessage.vue"; import TopMessage from "@/components/TopMessage.vue";
import { import { NotificationIface } from "@/constants/app";
AppString,
NotificationIface,
PASSKEYS_ENABLED,
} from "@/constants/app";
import { db, accountsDB } from "@/db/index"; import { db, accountsDB } from "@/db/index";
import { Account } from "@/db/tables/accounts";
import { Contact } from "@/db/tables/contacts"; import { Contact } from "@/db/tables/contacts";
import { import {
BoundingBox, BoundingBox,
@@ -323,20 +326,17 @@ import {
MASTER_SETTINGS_KEY, MASTER_SETTINGS_KEY,
Settings, Settings,
} from "@/db/tables/settings"; } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";
import { import {
contactForDid, contactForDid,
containsNonHiddenDid, containsNonHiddenDid,
didInfoForContact, didInfoForContact,
fetchEndorserRateLimits, fetchEndorserRateLimits,
getHeaders,
getPlanFromCache, getPlanFromCache,
GiverReceiverInputInfo, GiverReceiverInputInfo,
GiveSummaryRecord, GiveSummaryRecord,
} from "@/libs/endorserServer"; } from "@/libs/endorserServer";
import { import { generateSaveAndActivateIdentity } from "@/libs/util";
generateSaveAndActivateIdentity,
registerSaveAndActivatePasskey,
} from "@/libs/util";
interface GiveRecordWithContactInfo extends GiveSummaryRecord { interface GiveRecordWithContactInfo extends GiveSummaryRecord {
giver: { giver: {
@@ -354,11 +354,6 @@ interface GiveRecordWithContactInfo extends GiveSummaryRecord {
} }
@Component({ @Component({
computed: {
App() {
return App;
},
},
components: { components: {
GiftedDialog, GiftedDialog,
GiftedPrompts, GiftedPrompts,
@@ -372,9 +367,6 @@ interface GiveRecordWithContactInfo extends GiveSummaryRecord {
export default class HomeView extends Vue { export default class HomeView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void; $notify!: (notification: NotificationIface, timeout?: number) => void;
AppString = AppString;
PASSKEYS_ENABLED = PASSKEYS_ENABLED;
activeDid = ""; activeDid = "";
allContacts: Array<Contact> = []; allContacts: Array<Contact> = [];
allMyDids: Array<string> = []; allMyDids: Array<string> = [];
@@ -382,7 +374,6 @@ export default class HomeView extends Vue {
feedData: GiveRecordWithContactInfo[] = []; feedData: GiveRecordWithContactInfo[] = [];
feedPreviousOldestId?: string; feedPreviousOldestId?: string;
feedLastViewedClaimId?: string; feedLastViewedClaimId?: string;
givenName = "";
isAnyFeedFilterOn: boolean; isAnyFeedFilterOn: boolean;
isCreatingIdentifier = false; isCreatingIdentifier = false;
isFeedFilteredByVisible = false; isFeedFilteredByVisible = false;
@@ -396,18 +387,30 @@ export default class HomeView extends Vue {
showShortcutBvc = false; showShortcutBvc = false;
userAgentInfo = new UAParser(); // see https://docs.uaparser.js.org/v2/api/ua-parser-js/get-os.html userAgentInfo = new UAParser(); // see https://docs.uaparser.js.org/v2/api/ua-parser-js/get-os.html
public async getIdentity(activeDid: string): Promise<IIdentifier | null> {
await accountsDB.open();
const account = (await accountsDB.accounts
.where("did")
.equals(activeDid)
.first()) as Account;
const identity = JSON.parse(account?.identity || "null");
return identity; // may be null
}
public async getHeaders(identity: IIdentifier) {
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
return headers;
}
async mounted() { async mounted() {
try { try {
await accountsDB.open(); await accountsDB.open();
const allAccounts = await accountsDB.accounts.toArray(); const allAccounts = await accountsDB.accounts.toArray();
if (allAccounts.length > 0) {
this.allMyDids = allAccounts.map((acc) => acc.did); this.allMyDids = allAccounts.map((acc) => acc.did);
} else {
this.isCreatingIdentifier = true;
const newDid = await generateSaveAndActivateIdentity();
this.isCreatingIdentifier = false;
this.allMyDids = [newDid];
}
await db.open(); await db.open();
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings; const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
@@ -415,7 +418,6 @@ export default class HomeView extends Vue {
this.activeDid = settings?.activeDid || ""; this.activeDid = settings?.activeDid || "";
this.allContacts = await db.contacts.toArray(); this.allContacts = await db.contacts.toArray();
this.feedLastViewedClaimId = settings?.lastViewedClaimId; this.feedLastViewedClaimId = settings?.lastViewedClaimId;
this.givenName = settings?.firstName || "";
this.isFeedFilteredByVisible = !!settings?.filterFeedByVisible; this.isFeedFilteredByVisible = !!settings?.filterFeedByVisible;
this.isFeedFilteredByNearby = !!settings?.filterFeedByNearby; this.isFeedFilteredByNearby = !!settings?.filterFeedByNearby;
this.isRegistered = !!settings?.isRegistered; this.isRegistered = !!settings?.isRegistered;
@@ -424,14 +426,21 @@ export default class HomeView extends Vue {
this.isAnyFeedFilterOn = isAnyFeedFilterOn(settings); this.isAnyFeedFilterOn = isAnyFeedFilterOn(settings);
if (this.allMyDids.length === 0) {
this.isCreatingIdentifier = true;
this.activeDid = await generateSaveAndActivateIdentity();
this.allMyDids = [this.activeDid];
this.isCreatingIdentifier = false;
}
// someone may have have registered after sharing contact info, so recheck // someone may have have registered after sharing contact info
if (!this.isRegistered && this.activeDid) { if (!this.isRegistered && this.activeDid) {
const identity = await this.getIdentity(this.activeDid);
try { try {
const resp = await fetchEndorserRateLimits( const resp = await fetchEndorserRateLimits(
this.apiServer, this.apiServer,
this.axios, this.axios,
this.activeDid, identity as IIdentifier,
); );
if (resp.status === 200) { if (resp.status === 200) {
// we just needed to know that they're registered // we just needed to know that they're registered
@@ -466,15 +475,6 @@ export default class HomeView extends Vue {
} }
} }
async generatePasskeyIdentifier() {
this.isCreatingIdentifier = true;
const account = await registerSaveAndActivatePasskey(
AppString.APP_NAME + (this.givenName ? " - " + this.givenName : ""),
);
this.activeDid = account.did;
this.allMyDids = this.allMyDids.concat(this.activeDid);
this.isCreatingIdentifier = false;
}
resultsAreFiltered() { resultsAreFiltered() {
return this.isFeedFilteredByVisible || this.isFeedFilteredByNearby; return this.isFeedFilteredByVisible || this.isFeedFilteredByNearby;
} }
@@ -483,6 +483,26 @@ export default class HomeView extends Vue {
return "Notification" in window; return "Notification" in window;
} }
public async buildHeaders() {
const headers: HeadersInit = {
"Content-Type": "application/json",
};
const identity = await this.getIdentity(this.activeDid);
if (this.activeDid) {
if (identity) {
headers["Authorization"] = "Bearer " + (await accessToken(identity));
} else {
throw new Error(
"An ID is chosen but there are no keys for it so it cannot be used to talk with the service. Switch your ID.",
);
}
} else {
// it's OK without auth... we just won't get any identifiers
}
return headers;
}
// only called when a setting was changed // only called when a setting was changed
async reloadFeedOnChange() { async reloadFeedOnChange() {
await db.open(); await db.open();
@@ -500,7 +520,7 @@ export default class HomeView extends Vue {
* Data loader used by infinite scroller * Data loader used by infinite scroller
* @param payload is the flag from the InfiniteScroll indicating if it should load * @param payload is the flag from the InfiniteScroll indicating if it should load
**/ **/
async loadMoreGives(payload: boolean) { public async loadMoreGives(payload: boolean) {
// Since feed now loads projects along the way, it takes longer // Since feed now loads projects along the way, it takes longer
// and the InfiniteScroll component triggers a load before finished. // and the InfiniteScroll component triggers a load before finished.
// One alternative is to totally separate the project link loading. // One alternative is to totally separate the project link loading.
@@ -522,7 +542,7 @@ export default class HomeView extends Vue {
} }
} }
async updateAllFeed() { public async updateAllFeed() {
this.isFeedLoading = true; this.isFeedLoading = true;
let endOfResults = true; let endOfResults = true;
await this.retrieveGives(this.apiServer, this.feedPreviousOldestId) await this.retrieveGives(this.apiServer, this.feedPreviousOldestId)
@@ -530,6 +550,7 @@ export default class HomeView extends Vue {
if (results.data.length > 0) { if (results.data.length > 0) {
endOfResults = false; endOfResults = false;
// include the descriptions of the giver and receiver // include the descriptions of the giver and receiver
const identity = await this.getIdentity(this.activeDid);
for (const record: GiveSummaryRecord of results.data) { for (const record: GiveSummaryRecord of results.data) {
// similar code is in endorser-mobile utility.ts // similar code is in endorser-mobile utility.ts
// claim.claim happen for some claims wrapped in a Verifiable Credential // claim.claim happen for some claims wrapped in a Verifiable Credential
@@ -546,9 +567,9 @@ export default class HomeView extends Vue {
// We should display it immediately and then get the plan later. // We should display it immediately and then get the plan later.
const plan = await getPlanFromCache( const plan = await getPlanFromCache(
record.fulfillsPlanHandleId, record.fulfillsPlanHandleId,
identity,
this.axios, this.axios,
this.apiServer, this.apiServer,
this.activeDid,
); );
// check if the record should be filtered out // check if the record should be filtered out
@@ -629,7 +650,7 @@ export default class HomeView extends Vue {
* @param beforeId the earliest ID (of previous searches) to search earlier * @param beforeId the earliest ID (of previous searches) to search earlier
* @return claims in reverse chronological order * @return claims in reverse chronological order
*/ */
async retrieveGives(endorserApiServer: string, beforeId?: string) { public async retrieveGives(endorserApiServer: string, beforeId?: string) {
const beforeQuery = beforeId == null ? "" : "&beforeId=" + beforeId; const beforeQuery = beforeId == null ? "" : "&beforeId=" + beforeId;
const response = await fetch( const response = await fetch(
endorserApiServer + endorserApiServer +
@@ -637,7 +658,7 @@ export default class HomeView extends Vue {
beforeQuery, beforeQuery,
{ {
method: "GET", method: "GET",
headers: await getHeaders(this.activeDid), headers: await this.buildHeaders(),
}, },
); );

View File

@@ -39,10 +39,10 @@
<!-- Other Identity/ies --> <!-- Other Identity/ies -->
<ul class="mb-4"> <ul class="mb-4">
<li v-for="ident in otherIdentities" :key="ident.did"> <li
<div class="flex items-center justify-between mb-2"> class="block bg-slate-100 rounded-md flex items-center px-4 py-3 mb-2"
<div v-for="ident in otherIdentities"
class="flex flex-grow items-center bg-slate-100 rounded-md px-4 py-3 mb-2 truncate cursor-pointer" :key="ident.did"
@click="switchAccount(ident.did)" @click="switchAccount(ident.did)"
> >
<fa <fa
@@ -50,32 +50,13 @@
icon="circle-check" icon="circle-check"
class="fa-fw text-blue-600 text-xl mr-3" class="fa-fw text-blue-600 text-xl mr-3"
/> />
<fa <fa v-else icon="circle" class="fa-fw text-slate-400 text-xl mr-3" />
v-else <span class="overflow-hidden">
icon="circle" <h2 class="text-xl font-semibold mb-0"></h2>
class="fa-fw text-slate-400 text-xl mr-3"
/>
<span class="flex-grow overflow-hidden">
<div class="text-sm text-slate-500 truncate"> <div class="text-sm text-slate-500 truncate">
<b>ID:</b> <code>{{ ident.did }}</code> <b>ID:</b> <code>{{ ident.did }}</code>
</div> </div>
</span> </span>
</div>
<div>
<fa
v-if="ident.did === activeDid"
icon="trash-can"
class="text-slate-400 text-xl ml-2 mr-2 cursor-pointer"
@click="notifyCannotDelete()"
/>
<fa
v-else
icon="trash-can"
class="text-red-600 text-xl ml-2 mr-2 cursor-pointer"
@click="deleteAccount(ident.id)"
/>
</div>
</div>
</li> </li>
</ul> </ul>
@@ -100,8 +81,9 @@
<script lang="ts"> <script lang="ts">
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { NotificationIface } from "@/constants/app"; import { AppString, NotificationIface } from "@/constants/app";
import { db, accountsDB } from "@/db/index"; import { db, accountsDB } from "@/db/index";
import { AccountsSchema } from "@/db/tables/accounts";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
@@ -109,11 +91,14 @@ import QuickNav from "@/components/QuickNav.vue";
export default class IdentitySwitcherView extends Vue { export default class IdentitySwitcherView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void; $notify!: (notification: NotificationIface, timeout?: number) => void;
Constants = AppString;
public accounts: typeof AccountsSchema;
public activeDid = ""; public activeDid = "";
public activeDidInIdentities = false; public activeDidInIdentities = false;
public apiServer = ""; public apiServer = "";
public apiServerInput = ""; public apiServerInput = "";
public otherIdentities: Array<{ id: string; did: string }> = []; public otherIdentities: Array<{ did: string }> = [];
public showContactGives = false;
async created() { async created() {
try { try {
@@ -122,15 +107,21 @@ export default class IdentitySwitcherView extends Vue {
this.activeDid = settings?.activeDid || ""; this.activeDid = settings?.activeDid || "";
this.apiServer = settings?.apiServer || ""; this.apiServer = settings?.apiServer || "";
this.apiServerInput = settings?.apiServer || ""; this.apiServerInput = settings?.apiServer || "";
this.showContactGives = !!settings?.showContactGivesInline;
await accountsDB.open(); await accountsDB.open();
const accounts = await accountsDB.accounts.toArray(); const accounts = await accountsDB.accounts.toArray();
for (let n = 0; n < accounts.length; n++) { for (let n = 0; n < accounts.length; n++) {
const acct = accounts[n]; try {
this.otherIdentities.push({ id: acct.id as string, did: acct.did }); const did = accounts[n]["did"];
if (acct.did && this.activeDid === acct.did) { this.otherIdentities.push({ did: did });
if (did && this.activeDid === did) {
this.activeDidInIdentities = true; this.activeDidInIdentities = true;
} }
} catch (err) {
console.error("Error parsing identity:", err);
continue;
}
} }
} catch (err) { } catch (err) {
this.$notify( this.$notify(
@@ -157,36 +148,5 @@ export default class IdentitySwitcherView extends Vue {
}); });
this.$router.push({ name: "account" }); this.$router.push({ name: "account" });
} }
async deleteAccount(id: string) {
this.$notify(
{
group: "modal",
type: "confirm",
title: "Delete Identity?",
text: "Are you sure you want to erase this identity? (There is no undo. You may want to select it and back it up just in case.)",
onYes: async () => {
await accountsDB.open();
await accountsDB.accounts.delete(id);
this.otherIdentities = this.otherIdentities.filter(
(ident) => ident.id !== id,
);
},
},
-1,
);
}
notifyCannotDelete() {
this.$notify(
{
group: "alert",
type: "warning",
title: "Cannot Delete",
text: "You cannot delete the active identity.",
},
3000,
);
}
} }
</script> </script>

View File

@@ -17,7 +17,7 @@
<div> <div>
<p class="text-center text-xl mb-4 font-light"> <p class="text-center text-xl mb-4 font-light">
Will increment the maximum known derivation path from the existing seed. Will increment the maximum derivation path from the existing seed.
</p> </p>
<p v-if="didArrays.length > 1"> <p v-if="didArrays.length > 1">
@@ -75,7 +75,7 @@ import {
deriveAddress, deriveAddress,
newIdentifier, newIdentifier,
nextDerivationPath, nextDerivationPath,
} from "@/libs/crypto"; } from "../libs/crypto";
import { accountsDB, db } from "@/db/index"; import { accountsDB, db } from "@/db/index";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";

View File

@@ -68,6 +68,8 @@ export default class NewEditAccountView extends Vue {
firstName: this.givenName, firstName: this.givenName,
lastName: "", // deprecated, pre v 0.1.3 lastName: "", // deprecated, pre v 0.1.3
}); });
localStorage.setItem("firstName", this.givenName as string);
localStorage.setItem("lastName", ""); // deprecated, pre v 0.1.3
this.$router.back(); this.$router.back();
} }

View File

@@ -173,22 +173,22 @@
<script lang="ts"> <script lang="ts">
import "leaflet/dist/leaflet.css"; import "leaflet/dist/leaflet.css";
import { AxiosError, AxiosRequestHeaders } from "axios"; import { AxiosError } from "axios";
import * as didJwt from "did-jwt";
import { DateTime } from "luxon"; import { DateTime } from "luxon";
import { IIdentifier } from "@veramo/core";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { LMap, LMarker, LTileLayer } from "@vue-leaflet/vue-leaflet"; import { LMap, LMarker, LTileLayer } from "@vue-leaflet/vue-leaflet";
import ImageMethodDialog from "@/components/ImageMethodDialog.vue";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "@/constants/app"; import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "@/constants/app";
import { accountsDB, db } from "@/db/index"; import { accountsDB, db } from "@/db/index";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { import { accessToken, SimpleSigner } from "@/libs/crypto";
createEndorserJwtVcFromClaim, import * as libsUtil from "@/libs/util";
getHeaders,
PlanVerifiableCredential,
} from "@/libs/endorserServer";
import { useAppStore } from "@/store/app"; import { useAppStore } from "@/store/app";
import { PlanVerifiableCredential } from "@/libs/endorserServer";
import ImageMethodDialog from "@/components/ImageMethodDialog.vue";
@Component({ @Component({
components: { ImageMethodDialog, LMap, LMarker, LTileLayer, QuickNav }, components: { ImageMethodDialog, LMap, LMarker, LTileLayer, QuickNav },
@@ -227,6 +227,33 @@ export default class NewEditProjectView extends Vue {
zoneName = DateTime.local().zoneName; zoneName = DateTime.local().zoneName;
zoom = 2; zoom = 2;
libsUtil = libsUtil;
public async getIdentity(activeDid: string) {
await accountsDB.open();
const account = await accountsDB.accounts
.where("did")
.equals(activeDid)
.first();
const identity = JSON.parse((account?.identity as string) || "null");
if (!identity) {
throw new Error(
"Attempted to load project records with no identifier available.",
);
}
return identity;
}
public async getHeaders(identity: IIdentifier) {
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
return headers;
}
async mounted() { async mounted() {
await accountsDB.open(); await accountsDB.open();
this.numAccounts = await accountsDB.accounts.count(); this.numAccounts = await accountsDB.accounts.count();
@@ -240,17 +267,27 @@ export default class NewEditProjectView extends Vue {
if (this.numAccounts === 0) { if (this.numAccounts === 0) {
this.errNote("There was a problem loading your account info."); this.errNote("There was a problem loading your account info.");
} else { } else {
this.loadProject(this.activeDid); const identity = await this.getIdentity(this.activeDid);
if (!identity) {
throw new Error(
"An ID is chosen but there are no keys for it so it cannot be used to talk with the service. Switch your ID.",
);
}
this.loadProject(identity);
} }
} }
} }
async loadProject(userDid: string) { async loadProject(identity: IIdentifier) {
const url = const url =
this.apiServer + this.apiServer +
"/api/claim/byHandle/" + "/api/claim/byHandle/" +
encodeURIComponent(this.projectId); encodeURIComponent(this.projectId);
const headers = await getHeaders(userDid); const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
try { try {
const resp = await this.axios.get(url, { headers }); const resp = await this.axios.get(url, { headers });
@@ -305,12 +342,17 @@ export default class NewEditProjectView extends Vue {
return; return;
} }
try { try {
const headers = getHeaders(this.activeDid) as AxiosRequestHeaders; const identity = await libsUtil.getIdentity(this.activeDid);
const token = await accessToken(identity);
const response = await this.axios.delete( const response = await this.axios.delete(
DEFAULT_IMAGE_API_SERVER + DEFAULT_IMAGE_API_SERVER +
"/image/" + "/image/" +
encodeURIComponent(this.imageUrl), encodeURIComponent(this.imageUrl),
{ headers }, {
headers: {
Authorization: `Bearer ${token}`,
},
},
); );
if (response.status === 204) { if (response.status === 204) {
// don't bother with a notification // don't bother with a notification
@@ -353,7 +395,7 @@ export default class NewEditProjectView extends Vue {
} }
} }
private async saveProject(issuerDid: string) { private async saveProject(identity: IIdentifier) {
// Make a claim // Make a claim
const vcClaim: PlanVerifiableCredential = this.fullClaim; const vcClaim: PlanVerifiableCredential = this.fullClaim;
if (this.projectId) { if (this.projectId) {
@@ -404,13 +446,35 @@ export default class NewEditProjectView extends Vue {
} else { } else {
delete vcClaim.startTime; delete vcClaim.startTime;
} }
const vcJwt = await createEndorserJwtVcFromClaim(issuerDid, vcClaim); // Make a payload for the claim
const vcPayload = {
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 != null) {
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 // Make the xhr request payload
const payload = JSON.stringify({ jwtEncoded: vcJwt }); const payload = JSON.stringify({ jwtEncoded: vcJwt });
const url = this.apiServer + "/api/v2/claim"; const url = this.apiServer + "/api/v2/claim";
const headers = await getHeaders(issuerDid); const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
try { try {
const resp = await this.axios.post(url, payload, { headers }); const resp = await this.axios.post(url, payload, { headers });
@@ -469,7 +533,10 @@ export default class NewEditProjectView extends Vue {
); );
} }
} else { } else {
console.error("Here's the full error trying to save the claim:", error); console.error(
"Here's the full error trying to save the claim:",
error,
);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@@ -484,6 +551,7 @@ export default class NewEditProjectView extends Vue {
this.errorMessage = userMessage; this.errorMessage = userMessage;
} }
} }
}
public async onSaveProjectClick() { public async onSaveProjectClick() {
this.isHiddenSave = true; this.isHiddenSave = true;
@@ -492,7 +560,8 @@ export default class NewEditProjectView extends Vue {
if (this.numAccounts === 0) { if (this.numAccounts === 0) {
console.error("Error: there is no account."); console.error("Error: there is no account.");
} else { } else {
this.saveProject(this.activeDid); const identity = await this.getIdentity(this.activeDid);
this.saveProject(identity);
} }
} }

View File

@@ -256,7 +256,6 @@
contact above.) contact above.)
</div> </div>
<!-- similar to gift display below -->
<ul v-else class="text-sm border-t border-slate-300"> <ul v-else class="text-sm border-t border-slate-300">
<li <li
v-for="give in givesToThis" v-for="give in givesToThis"
@@ -264,8 +263,8 @@
class="py-1.5 border-b border-slate-300" class="py-1.5 border-b border-slate-300"
> >
<div class="flex justify-between gap-4"> <div class="flex justify-between gap-4">
<span> <span
<fa icon="user" class="fa-fw text-slate-400" /> ><fa icon="user" class="fa-fw text-slate-400"></fa>
{{ {{
serverUtil.didInfo( serverUtil.didInfo(
give.agentDid, give.agentDid,
@@ -309,62 +308,12 @@
</div> </div>
<div class="grid items-start grid-cols-1 gap-4"> <div class="grid items-start grid-cols-1 gap-4">
<div
v-if="givesProvidedByThis.length > 0"
class="bg-slate-100 px-4 py-3 rounded-md"
>
<h3 class="text-sm uppercase font-semibold mb-3 border-b">
Individuals Getting Contributions From This
</h3>
<!-- similar to gift display above -->
<ul class="text-sm border-t border-slate-300">
<li
v-for="give in givesProvidedByThis"
:key="give.id"
class="py-1.5 border-b border-slate-300"
>
<div class="flex justify-between gap-4">
<span>
{{
serverUtil.didInfo(
give.agentDid,
activeDid,
allMyDids,
allContacts,
)
}}
</span>
<span v-if="give.amount" class="whitespace-nowrap">
<fa
:icon="libsUtil.iconForUnitCode(give.unit)"
class="fa-fw text-slate-400"
/>{{ give.amount }}
</span>
</div>
<div class="text-slate-500">
<fa icon="calendar" class="fa-fw text-slate-400" />
{{ give.issuedAt?.substring(0, 10) }}
</div>
<div v-if="give.description" class="text-slate-500">
<fa icon="comment" class="fa-fw text-slate-400" />
{{ give.description }}
</div>
<a @click="onClickLoadClaim(give.jwtId)">
<fa icon="file-lines" class="text-blue-500 cursor-pointer" />
</a>
</li>
</ul>
<div v-if="givesProvidedByHitLimit" class="text-center">
<button @click="loadGivesProvidedBy()">Load More</button>
</div>
</div>
<div <div
v-if="fulfillersToThis.length > 0" v-if="fulfillersToThis.length > 0"
class="bg-slate-100 px-4 py-3 rounded-md" class="bg-slate-100 px-4 py-3 rounded-md"
> >
<h3 class="text-sm uppercase font-semibold mb-3"> <h3 class="text-sm uppercase font-semibold mb-3">
Projects That Contribute To This Contributions To This Idea
</h3> </h3>
<!-- centering because long, wrapped project names didn't left align with blank or "text-left" --> <!-- centering because long, wrapped project names didn't left align with blank or "text-left" -->
<div class="text-center"> <div class="text-center">
@@ -376,15 +325,13 @@
{{ plan.name }} {{ plan.name }}
</button> </button>
</div> </div>
<div v-if="fulfillersToHitLimit" class="text-center"> <div v-if="fulfillersToHitLimit" class="text-center">Load More</div>
<button @click="loadPlanFulfillersTo()">Load More</button>
</div>
</div> </div>
</div> </div>
<div v-if="fulfilledByThis" class="bg-slate-100 px-4 py-3 rounded-md"> <div v-if="fulfilledByThis" class="bg-slate-100 px-4 py-3 rounded-md">
<h3 class="text-sm uppercase font-semibold mb-3"> <h3 class="text-sm uppercase font-semibold mb-3">
Projects Getting Contributions From This Contributions From This Idea
</h3> </h3>
<!-- centering because long, wrapped project names didn't left align with blank or "text-left" --> <!-- centering because long, wrapped project names didn't left align with blank or "text-left" -->
<div class="text-center"> <div class="text-center">
@@ -402,7 +349,8 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { AxiosError } from "axios"; import { AxiosError, RawAxiosRequestHeaders } from "axios";
import { IIdentifier } from "@veramo/core";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import GiftedDialog from "@/components/GiftedDialog.vue"; import GiftedDialog from "@/components/GiftedDialog.vue";
@@ -416,11 +364,11 @@ import { accountsDB, db } from "@/db/index";
import { Account } from "@/db/tables/accounts"; import { Account } from "@/db/tables/accounts";
import { Contact } from "@/db/tables/contacts"; import { Contact } from "@/db/tables/contacts";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";
import * as libsUtil from "@/libs/util"; import * as libsUtil from "@/libs/util";
import { import {
BLANK_GENERIC_SERVER_RECORD, BLANK_GENERIC_SERVER_RECORD,
GenericCredWrapper, GenericCredWrapper,
getHeaders,
GiverReceiverInputInfo, GiverReceiverInputInfo,
GiveSummaryRecord, GiveSummaryRecord,
OfferSummaryRecord, OfferSummaryRecord,
@@ -453,8 +401,6 @@ export default class ProjectViewView extends Vue {
fulfillersToHitLimit = false; fulfillersToHitLimit = false;
givesToThis: Array<GiveSummaryRecord> = []; givesToThis: Array<GiveSummaryRecord> = [];
givesHitLimit = false; givesHitLimit = false;
givesProvidedByThis: Array<GiveSummaryRecord> = [];
givesProvidedByHitLimit = false;
imageUrl = ""; imageUrl = "";
issuer = ""; issuer = "";
latitude = 0; latitude = 0;
@@ -483,12 +429,24 @@ export default class ProjectViewView extends Vue {
const accounts = accountsDB.accounts; const accounts = accountsDB.accounts;
const accountsArr: Account[] = await accounts?.toArray(); const accountsArr: Account[] = await accounts?.toArray();
this.allMyDids = accountsArr.map((acc) => acc.did); this.allMyDids = accountsArr.map((acc) => acc.did);
const account = accountsArr.find((acc) => acc.did === this.activeDid);
const identity = JSON.parse((account?.identity as string) || "null");
const pathParam = window.location.pathname.substring("/project/".length); const pathParam = window.location.pathname.substring("/project/".length);
if (pathParam) { if (pathParam) {
this.projectId = decodeURIComponent(pathParam); this.projectId = decodeURIComponent(pathParam);
} }
this.loadProject(this.projectId, this.activeDid); this.loadProject(this.projectId, identity);
}
public async getIdentity(activeDid: string) {
await accountsDB.open();
const account = (await accountsDB.accounts
.where("did")
.equals(activeDid)
.first()) as Account;
const identity = JSON.parse(account?.identity || "null");
return identity;
} }
onEditClick() { onEditClick() {
@@ -508,12 +466,18 @@ export default class ProjectViewView extends Vue {
this.expanded = false; this.expanded = false;
} }
async loadProject(projectId: string, userDid: string) { async loadProject(projectId: string, identity: IIdentifier) {
this.projectId = projectId; this.projectId = projectId;
const url = const url =
this.apiServer + "/api/claim/byHandle/" + encodeURIComponent(projectId); this.apiServer + "/api/claim/byHandle/" + encodeURIComponent(projectId);
const headers = await getHeaders(userDid); const headers: RawAxiosRequestHeaders = {
"Content-Type": "application/json",
};
if (identity) {
const token = await accessToken(identity);
headers["Authorization"] = "Bearer " + token;
}
try { try {
const resp = await this.axios.get(url, { headers }); const resp = await this.axios.get(url, { headers });
@@ -576,12 +540,15 @@ export default class ProjectViewView extends Vue {
this.loadGives(); this.loadGives();
this.loadGivesProvidedBy();
this.loadOffers(); this.loadOffers();
this.loadPlanFulfillersTo(); this.loadFulfillersTo();
// now load fulfilled-by, a single project
if (identity) {
const token = await accessToken(identity);
headers["Authorization"] = "Bearer " + token;
}
const fulfilledByUrl = const fulfilledByUrl =
this.apiServer + this.apiServer +
"/api/v2/report/planFulfilledByPlan?planHandleId=" + "/api/v2/report/planFulfilledByPlan?planHandleId=" +
@@ -631,7 +598,15 @@ export default class ProjectViewView extends Vue {
} }
const givesInUrl = givesUrl + postfix; const givesInUrl = givesUrl + postfix;
const headers = await getHeaders(this.activeDid); const headers: RawAxiosRequestHeaders = {
"Content-Type": "application/json",
};
const identity = await this.getIdentity(this.activeDid);
if (identity) {
const token = await accessToken(identity);
headers["Authorization"] = "Bearer " + token;
}
try { try {
const resp = await this.axios.get(givesInUrl, { headers }); const resp = await this.axios.get(givesInUrl, { headers });
if (resp.status === 200 && resp.data.data) { if (resp.status === 200 && resp.data.data) {
@@ -678,7 +653,15 @@ export default class ProjectViewView extends Vue {
} }
const offersInUrl = offersUrl + postfix; const offersInUrl = offersUrl + postfix;
const headers = await getHeaders(this.activeDid); const headers: RawAxiosRequestHeaders = {
"Content-Type": "application/json",
};
const identity = await this.getIdentity(this.activeDid);
if (identity) {
const token = await accessToken(identity);
headers["Authorization"] = "Bearer " + token;
}
try { try {
const resp = await this.axios.get(offersInUrl, { headers }); const resp = await this.axios.get(offersInUrl, { headers });
if (resp.status === 200 && resp.data.data) { if (resp.status === 200 && resp.data.data) {
@@ -713,7 +696,7 @@ export default class ProjectViewView extends Vue {
} }
} }
async loadPlanFulfillersTo() { async loadFulfillersTo() {
const fulfillsUrl = const fulfillsUrl =
this.apiServer + this.apiServer +
"/api/v2/report/planFulfillersToPlan?planHandleId=" + "/api/v2/report/planFulfillersToPlan?planHandleId=" +
@@ -726,7 +709,15 @@ export default class ProjectViewView extends Vue {
} }
const fulfillsInUrl = fulfillsUrl + postfix; const fulfillsInUrl = fulfillsUrl + postfix;
const headers = await getHeaders(this.activeDid); const headers: RawAxiosRequestHeaders = {
"Content-Type": "application/json",
};
const identity = await this.getIdentity(this.activeDid);
if (identity) {
const token = await accessToken(identity);
headers["Authorization"] = "Bearer " + token;
}
try { try {
const resp = await this.axios.get(fulfillsInUrl, { headers }); const resp = await this.axios.get(fulfillsInUrl, { headers });
if (resp.status === 200) { if (resp.status === 200) {
@@ -761,56 +752,6 @@ export default class ProjectViewView extends Vue {
} }
} }
async loadGivesProvidedBy() {
const providedByUrl =
this.apiServer +
"/api/v2/report/givesProvidedBy?providerId=" +
encodeURIComponent(this.projectId);
let postfix = "";
if (this.givesProvidedByThis.length > 0) {
postfix =
"&beforeId=" +
this.givesProvidedByThis[this.givesProvidedByThis.length - 1].jwtId;
}
const providedByFullUrl = providedByUrl + postfix;
const headers = await getHeaders(this.activeDid);
try {
const resp = await this.axios.get(providedByFullUrl, { headers });
if (resp.status === 200) {
this.givesProvidedByThis = this.givesProvidedByThis.concat(
resp.data.data,
);
this.givesProvidedByHitLimit = resp.data.hitLimit;
} else {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "Failed to retrieve gives that were provided by this project.",
},
5000,
);
}
} catch (error: unknown) {
const serverError = error as AxiosError;
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "Something went wrong retrieving gives that were provided by this project.",
},
5000,
);
console.error(
"Something went wrong retrieving gives that were provided by this project:",
serverError.message,
);
}
}
/** /**
* Handle clicking on a project entry found in the list * Handle clicking on a project entry found in the list
* @param id of the project * @param id of the project
@@ -821,7 +762,7 @@ export default class ProjectViewView extends Vue {
path: "/project/" + encodeURIComponent(projectId), path: "/project/" + encodeURIComponent(projectId),
}; };
this.$router.push(route); this.$router.push(route);
this.loadProject(projectId, this.activeDid); this.loadProject(projectId, await this.getIdentity(this.activeDid));
} }
getOpenStreetMapUrl() { getOpenStreetMapUrl() {
@@ -965,7 +906,7 @@ export default class ProjectViewView extends Vue {
}; };
const result = await serverUtil.createAndSubmitClaim( const result = await serverUtil.createAndSubmitClaim(
confirmationClaim, confirmationClaim,
this.activeDid, await this.getIdentity(this.activeDid),
this.apiServer, this.apiServer,
this.axios, this.axios,
); );

View File

@@ -233,16 +233,14 @@ import { Component, Vue } from "vue-facing-decorator";
import { NotificationIface } from "@/constants/app"; import { NotificationIface } from "@/constants/app";
import { accountsDB, db } from "@/db/index"; import { accountsDB, db } from "@/db/index";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";
import * as libsUtil from "@/libs/util"; import * as libsUtil from "@/libs/util";
import { IIdentifier } from "@veramo/core";
import InfiniteScroll from "@/components/InfiniteScroll.vue"; import InfiniteScroll from "@/components/InfiniteScroll.vue";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
import ProjectIcon from "@/components/ProjectIcon.vue"; import ProjectIcon from "@/components/ProjectIcon.vue";
import TopMessage from "@/components/TopMessage.vue"; import TopMessage from "@/components/TopMessage.vue";
import { import { OfferSummaryRecord, PlanData } from "@/libs/endorserServer";
getHeaders,
OfferSummaryRecord,
PlanData,
} from "@/libs/endorserServer";
import EntityIcon from "@/components/EntityIcon.vue"; import EntityIcon from "@/components/EntityIcon.vue";
@Component({ @Component({
@@ -257,9 +255,9 @@ export default class ProjectsView extends Vue {
); );
} }
activeDid = "";
apiServer = ""; apiServer = "";
projects: PlanData[] = []; projects: PlanData[] = [];
currentIid: IIdentifier;
isLoading = false; isLoading = false;
isRegistered = false; isRegistered = false;
numAccounts = 0; numAccounts = 0;
@@ -273,7 +271,7 @@ export default class ProjectsView extends Vue {
try { try {
await db.open(); await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY); const settings = await db.settings.get(MASTER_SETTINGS_KEY);
this.activeDid = (settings?.activeDid as string) || ""; const activeDid: string = (settings?.activeDid as string) || "";
this.apiServer = (settings?.apiServer as string) || ""; this.apiServer = (settings?.apiServer as string) || "";
this.isRegistered = !!settings?.isRegistered; this.isRegistered = !!settings?.isRegistered;
@@ -283,6 +281,7 @@ export default class ProjectsView extends Vue {
console.error("No accounts found."); console.error("No accounts found.");
this.errNote("You need an identifier to load your projects."); this.errNote("You need an identifier to load your projects.");
} else { } else {
this.currentIid = await this.getIdentity(activeDid);
await this.loadOffers(); await this.loadOffers();
} }
} catch (err) { } catch (err) {
@@ -296,9 +295,13 @@ export default class ProjectsView extends Vue {
* @param url the url used to fetch the data * @param url the url used to fetch the data
* @param token Authorization token * @param token Authorization token
**/ **/
async projectDataLoader(url: string) { async projectDataLoader(url: string, token: string) {
const headers: { [key: string]: string } = {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
};
try { try {
const headers = await getHeaders(this.activeDid);
this.isLoading = true; this.isLoading = true;
const resp = await this.axios.get(url, { headers } as AxiosRequestConfig); const resp = await this.axios.get(url, { headers } as AxiosRequestConfig);
if (resp.status === 200 && resp.data.data) { if (resp.status === 200 && resp.data.data) {
@@ -339,7 +342,7 @@ export default class ProjectsView extends Vue {
if (this.projects.length > 0 && payload) { if (this.projects.length > 0 && payload) {
const latestProject = this.projects[this.projects.length - 1]; const latestProject = this.projects[this.projects.length - 1];
await this.loadProjects( await this.loadProjects(
this.activeDid, this.currentIid,
`beforeId=${latestProject.rowid}`, `beforeId=${latestProject.rowid}`,
); );
} }
@@ -347,12 +350,30 @@ export default class ProjectsView extends Vue {
/** /**
* Load projects initially * Load projects initially
* @param issuerDid of the user * @param identifier of the user
* @param urlExtra additional url parameters in a string * @param urlExtra additional url parameters in a string
**/ **/
async loadProjects(activeDid?: string, urlExtra: string = "") { async loadProjects(identifier?: IIdentifier, urlExtra: string = "") {
const identity = identifier || this.currentIid;
const url = `${this.apiServer}/api/v2/report/plansByIssuer?${urlExtra}`; const url = `${this.apiServer}/api/v2/report/plansByIssuer?${urlExtra}`;
await this.projectDataLoader(url); const token: string = await accessToken(identity);
await this.projectDataLoader(url, token);
}
public async getIdentity(activeDid: string): Promise<IIdentifier> {
await accountsDB.open();
const account = await accountsDB.accounts
.where("did")
.equals(activeDid)
.first();
const identity = JSON.parse((account?.identity as string) || "null");
if (!identity) {
throw new Error(
"Attempted to load project records with no identifier available.",
);
}
return identity;
} }
/** /**
@@ -390,8 +411,11 @@ export default class ProjectsView extends Vue {
* @param url the url used to fetch the data * @param url the url used to fetch the data
* @param token Authorization token * @param token Authorization token
**/ **/
async offerDataLoader(url: string) { async offerDataLoader(url: string, token: string) {
const headers = getHeaders(this.activeDid); const headers: { [key: string]: string } = {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
};
try { try {
this.isLoading = true; this.isLoading = true;
@@ -438,18 +462,20 @@ export default class ProjectsView extends Vue {
async loadMoreOfferData(payload: boolean) { async loadMoreOfferData(payload: boolean) {
if (this.offers.length > 0 && payload) { if (this.offers.length > 0 && payload) {
const latestOffer = this.offers[this.offers.length - 1]; const latestOffer = this.offers[this.offers.length - 1];
await this.loadOffers(this.activeDid, `&beforeId=${latestOffer.jwtId}`); await this.loadOffers(this.currentIid, `&beforeId=${latestOffer.jwtId}`);
} }
} }
/** /**
* Load offers initially * Load offers initially
* @param issuerDid of the user * @param identifier of the user
* @param urlExtra additional url parameters in a string * @param urlExtra additional url parameters in a string
**/ **/
async loadOffers(issuerDid?: string, urlExtra: string = "") { async loadOffers(identifier?: IIdentifier, urlExtra: string = "") {
const url = `${this.apiServer}/api/v2/report/offers?offeredByDid=${issuerDid}${urlExtra}`; const identity = identifier || this.currentIid;
await this.offerDataLoader(url); const url = `${this.apiServer}/api/v2/report/offers?offeredByDid=${identity.did}${urlExtra}`;
const token: string = await accessToken(identity);
await this.offerDataLoader(url, token);
} }
public computedOfferTabClassNames() { public computedOfferTabClassNames() {

View File

@@ -124,6 +124,7 @@ export default class QuickActionBvcBeginView extends Vue {
try { try {
const hoursNum = libsUtil.numberOrZero(this.hoursStr); const hoursNum = libsUtil.numberOrZero(this.hoursStr);
const identity = await libsUtil.getIdentity(activeDid);
this.$notify({ group: "alert", type: "toast", title: "Sent..." }, 1000); this.$notify({ group: "alert", type: "toast", title: "Sent..." }, 1000);
@@ -133,7 +134,7 @@ export default class QuickActionBvcBeginView extends Vue {
const timeResult = await createAndSubmitGive( const timeResult = await createAndSubmitGive(
axios, axios,
apiServer, apiServer,
activeDid, identity,
activeDid, activeDid,
undefined, undefined,
undefined, undefined,
@@ -164,7 +165,7 @@ export default class QuickActionBvcBeginView extends Vue {
if (this.attended) { if (this.attended) {
const attendResult = await createAndSubmitClaim( const attendResult = await createAndSubmitClaim(
bvcMeetingJoinClaim(activeDid, this.todayOrPreviousStartDate), bvcMeetingJoinClaim(activeDid, this.todayOrPreviousStartDate),
activeDid, identity,
apiServer, apiServer,
axios, axios,
); );

View File

@@ -138,25 +138,28 @@
import axios from "axios"; import axios from "axios";
import { DateTime } from "luxon"; import { DateTime } from "luxon";
import * as R from "ramda"; import * as R from "ramda";
import { IIdentifier } from "@veramo/core";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
import TopMessage from "@/components/TopMessage.vue"; import TopMessage from "@/components/TopMessage.vue";
import { NotificationIface } from "@/constants/app"; import { NotificationIface } from "@/constants/app";
import { accountsDB, db } from "@/db/index"; import { accountsDB, db } from "@/db/index";
import { Account } from "@/db/tables/accounts";
import { Contact } from "@/db/tables/contacts"; import { Contact } from "@/db/tables/contacts";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";
import { import {
BVC_MEETUPS_PROJECT_CLAIM_ID, BVC_MEETUPS_PROJECT_CLAIM_ID,
claimSpecialDescription, claimSpecialDescription,
containsHiddenDid, containsHiddenDid,
createAndSubmitConfirmation, createAndSubmitConfirmation,
createAndSubmitGive, createAndSubmitGive,
ErrorResult,
GenericCredWrapper, GenericCredWrapper,
GenericVerifiableCredential, GenericVerifiableCredential,
getHeaders,
ErrorResult,
} from "@/libs/endorserServer"; } from "@/libs/endorserServer";
import * as libsUtil from "@/libs/util";
@Component({ @Component({
methods: { claimSpecialDescription }, methods: { claimSpecialDescription },
@@ -210,7 +213,16 @@ export default class QuickActionBvcBeginView extends Vue {
await accountsDB.open(); await accountsDB.open();
const allAccounts = await accountsDB.accounts.toArray(); const allAccounts = await accountsDB.accounts.toArray();
this.allMyDids = allAccounts.map((acc) => acc.did); this.allMyDids = allAccounts.map((acc) => acc.did);
const headers = await getHeaders(this.activeDid); const account: Account | undefined = await accountsDB.accounts
.where("did")
.equals(this.activeDid)
.first();
const identity: IIdentifier = JSON.parse(
(account?.identity as string) || "null",
);
const headers = {
Authorization: "Bearer " + (await accessToken(identity)),
};
try { try {
const response = await fetch( const response = await fetch(
this.apiServer + this.apiServer +
@@ -263,6 +275,8 @@ export default class QuickActionBvcBeginView extends Vue {
async record() { async record() {
try { try {
const identity = await libsUtil.getIdentity(this.activeDid);
this.$notify({ group: "alert", type: "toast", title: "Sent..." }, 1000); this.$notify({ group: "alert", type: "toast", title: "Sent..." }, 1000);
// in parallel, make a confirmation for each selected claim and send them all to the server // in parallel, make a confirmation for each selected claim and send them all to the server
@@ -274,8 +288,9 @@ export default class QuickActionBvcBeginView extends Vue {
if (!record) { if (!record) {
return { type: "error", error: "Record not found." }; return { type: "error", error: "Record not found." };
} }
const identity = await libsUtil.getIdentity(this.activeDid);
return createAndSubmitConfirmation( return createAndSubmitConfirmation(
this.activeDid, identity,
record.claim as GenericVerifiableCredential, record.claim as GenericVerifiableCredential,
record.id, record.id,
record.handleId, record.handleId,
@@ -309,7 +324,7 @@ export default class QuickActionBvcBeginView extends Vue {
const giveResult = await createAndSubmitGive( const giveResult = await createAndSubmitGive(
axios, axios,
this.apiServer, this.apiServer,
this.activeDid, identity,
undefined, undefined,
this.activeDid, this.activeDid,
this.description, this.description,

View File

@@ -65,7 +65,8 @@ import {
} from "@/constants/app"; } from "@/constants/app";
import { db } from "@/db/index"; import { db } from "@/db/index";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { getHeaders } from "@/libs/endorserServer"; import { getIdentity } from "@/libs/util";
import { accessToken } from "@/libs/crypto";
@Component({ components: { PhotoDialog, QuickNav } }) @Component({ components: { PhotoDialog, QuickNav } })
export default class SharedPhotoView extends Vue { export default class SharedPhotoView extends Vue {
@@ -151,7 +152,11 @@ export default class SharedPhotoView extends Vue {
let result; let result;
try { try {
// send the image to the server // send the image to the server
const headers = await getHeaders(this.activeDid); const identifier = await getIdentity(this.activeDid as string);
const token = await accessToken(identifier);
const headers = {
Authorization: "Bearer " + token,
};
const formData = new FormData(); const formData = new FormData();
formData.append( formData.append(
"image", "image",

View File

@@ -17,7 +17,7 @@
<!-- Heading --> <!-- Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8"> <h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
Generate an Identity Start Here
</h1> </h1>
</div> </div>
@@ -25,58 +25,33 @@
<div id="start-question" class="mt-8"> <div id="start-question" class="mt-8">
<div class="max-w-3xl mx-auto"> <div class="max-w-3xl mx-auto">
<p class="text-center text-xl font-light"> <p class="text-center text-xl font-light">
How do you want to create this identifier? Do you want a new identifier of your own?
</p> </p>
<p v-if="PASSKEYS_ENABLED" class="text-center font-light mt-6"> <p class="text-center font-light">
A <strong>passkey</strong> is easy to manage, though it is less If you haven't used this before, click "Yes" to generate a new
interoperable with other systems for advanced uses. identifier.
<a
href="https://www.perplexity.ai/search/what-are-passkeys-v2SHV3yLQlyA2CYH6.Nvhg"
target="_blank"
>
<fa icon="info-circle" class="fa-fw text-blue-500" />
</a>
</p> </p>
<p class="text-center font-light mt-4"> <p class="text-center mb-4 font-light">
A <strong>new seed</strong> allows you full control over the keys, Only click "No" if you have a seed of 12 or 24 words generated
though you are responsible for backups. elsewhere.
<a
href="https://www.perplexity.ai/search/what-is-a-seed-phrase-OqiP9foVRXidr_2le5OFKA"
target="_blank"
>
<fa icon="info-circle" class="fa-fw text-blue-500" />
</a>
</p> </p>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2 mt-4">
<a <a
v-if="PASSKEYS_ENABLED" @click="onClickYes()"
@click="onClickNewPasskey()" class="block w-full text-center text-lg uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md mb-2"
class="block w-full text-center text-lg uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md mb-2 cursor-pointer"
> >
Generate one with a passkey Yes, generate one
</a> </a>
<a <div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
@click="onClickNewSeed()"
class="block w-full text-center text-lg uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md mb-2 cursor-pointer"
>
Generate one with a new seed
</a>
</div>
<p class="text-center font-light mt-4">
You can also import an existing seed or derive a new address from an
existing seed.
</p>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2 mt-2">
<a <a
@click="onClickNo()" @click="onClickNo()"
class="block w-full text-center text-md uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md cursor-pointer" class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md"
> >
You have a seed No, I have a seed
</a> </a>
<a <a
v-if="numAccounts > 0" v-if="numAccounts > 0"
@click="onClickDerive()" @click="onClickDerive()"
class="block w-full text-center text-md uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md cursor-pointer" class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md"
> >
Derive new address from existing seed Derive new address from existing seed
</a> </a>
@@ -89,40 +64,23 @@
<script lang="ts"> <script lang="ts">
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { AppString, PASSKEYS_ENABLED } from "@/constants/app"; import { accountsDB } from "@/db/index";
import { accountsDB, db } from "@/db/index";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { registerSaveAndActivatePasskey } from "@/libs/util";
@Component({ @Component({
components: {}, components: {},
}) })
export default class StartView extends Vue { export default class StartView extends Vue {
PASSKEYS_ENABLED = PASSKEYS_ENABLED;
givenName = "";
numAccounts = 0; numAccounts = 0;
async mounted() { async mounted() {
await db.open();
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
this.givenName = settings?.firstName || "";
await accountsDB.open(); await accountsDB.open();
this.numAccounts = await accountsDB.accounts.count(); this.numAccounts = await accountsDB.accounts.count();
} }
public onClickNewSeed() { public onClickYes() {
this.$router.push({ name: "new-identifier" }); this.$router.push({ name: "new-identifier" });
} }
public async onClickNewPasskey() {
const keyName =
AppString.APP_NAME + (this.givenName ? " - " + this.givenName : "");
await registerSaveAndActivatePasskey(keyName);
this.$router.push({ name: "account" });
}
public onClickNo() { public onClickNo() {
this.$router.push({ name: "import-account" }); this.$router.push({ name: "import-account" });
} }

View File

@@ -174,64 +174,58 @@
<h2 class="text-xl font-bold mb-4">Passkeys</h2> <h2 class="text-xl font-bold mb-4">Passkeys</h2>
See console for results. See console for results.
<br/> <br/>
See existing passkeys in Chrome at: chrome://settings/passkeys Active DID: {{ activeDid }}
<br /> {{ credIdHex ? "has passkey ID" : "has no passkey ID" }}
Active DID: {{ activeDid || "nothing, which" }}
{{ credIdHex ? "has a passkey ID" : "has no passkey ID" }}
<div> <div>
Register Passkey Register
<button <button
@click="register()" @click="register()"
class="font-bold uppercase bg-slate-500 text-white px-3 py-2 rounded-md mr-2" class="font-bold uppercase bg-slate-600 text-white px-3 py-2 rounded-md mr-2"
> >
Simplewebauthn Simplewebauthn
</button> </button>
</div> </div>
<div> <div>
Create JWT Create
<button <button
@click="createJwtSimplewebauthn()" @click="createJwtSimplewebauthn()"
class="font-bold uppercase bg-slate-500 text-white px-3 py-2 rounded-md mr-2" class="font-bold uppercase bg-slate-600 text-white px-3 py-2 rounded-md mr-2"
> >
Simplewebauthn Simplewebauthn
</button> </button>
<button <button
@click="createJwtNavigator()" @click="createJwtNavigator()"
class="font-bold uppercase bg-slate-500 text-white px-3 py-2 rounded-md mr-2" class="font-bold uppercase bg-slate-600 text-white px-3 py-2 rounded-md mr-2"
> >
Navigator Navigator
</button> </button>
</div> </div>
<div v-if="jwt"> <div v-if="jwt">
Verify New JWT Verify
<button <button
@click="verifySimplewebauthn()" @click="verifySimplewebauthn()"
class="font-bold uppercase bg-slate-500 text-white px-3 py-2 rounded-md mr-2" class="font-bold uppercase bg-slate-600 text-white px-3 py-2 rounded-md mr-2"
> >
Simplewebauthn Simplewebauthn
</button> </button>
<button <button
@click="verifyWebCrypto()" @click="verifyWebCrypto()"
class="font-bold uppercase bg-slate-500 text-white px-3 py-2 rounded-md mr-2" class="font-bold uppercase bg-slate-600 text-white px-3 py-2 rounded-md mr-2"
> >
WebCrypto WebCrypto
</button> </button>
<button <button
@click="verifyP256()" @click="verifyP256()"
class="font-bold uppercase bg-slate-500 text-white px-3 py-2 rounded-md mr-2" class="font-bold uppercase bg-slate-600 text-white px-3 py-2 rounded-md mr-2"
> >
p256 - broken p256 - broken
</button> </button>
</div> </div>
<div v-else>Verify New JWT -- requires creation first</div>
<button <button
@click="verifyMyJwt()" @click="verifyMyJwt()"
class="font-bold uppercase bg-slate-500 text-white px-3 py-2 rounded-md mr-2" class="font-bold uppercase bg-slate-600 text-white px-3 py-2 rounded-md mr-2"
> >
Verify Hard-Coded JWT Verify Mine
</button> </button>
</div> </div>
</section> </section>
@@ -244,17 +238,16 @@ import { ref } from "vue";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
import { AppString, NotificationIface } from "@/constants/app";
import { accountsDB, db } from "@/db/index"; import { accountsDB, db } from "@/db/index";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import * as vcLib from "@/libs/crypto/vc";
import { import {
createPeerDid,
PeerSetup, PeerSetup,
registerCredential,
verifyJwtP256, verifyJwtP256,
verifyJwtSimplewebauthn, verifyJwtSimplewebauthn,
verifyJwtWebCrypto, verifyJwtWebCrypto,
} from "@/libs/crypto/vc/passkeyDidPeer"; } from "@/libs/didPeer";
import {AccountKeyInfo, getAccount, registerAndSavePasskey} from "@/libs/util"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
const inputFileNameRef = ref<Blob>(); const inputFileNameRef = ref<Blob>();
@@ -270,8 +263,6 @@ const TEST_PAYLOAD = {
@Component({ components: { QuickNav } }) @Component({ components: { QuickNav } })
export default class Help extends Vue { export default class Help extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
// for file import // for file import
fileName?: string; fileName?: string;
@@ -303,7 +294,7 @@ export default class Help extends Vue {
} }
async uploadFile(event: Event) { async uploadFile(event: Event) {
inputFileNameRef.value = event.target?.["files"][0]; inputFileNameRef.value = event.target.files[0];
// https://developer.mozilla.org/en-US/docs/Web/API/File // https://developer.mozilla.org/en-US/docs/Web/API/File
// ... plus it has a `type` property from my testing // ... plus it has a `type` property from my testing
const file = inputFileNameRef.value; const file = inputFileNameRef.value;
@@ -333,41 +324,21 @@ export default class Help extends Vue {
} }
public async register() { public async register() {
const DEFAULT_USERNAME = AppString.APP_NAME + " Tester"; const cred = await registerCredential(this.userName);
if (!this.userName) { const publicKeyBytes = cred.publicKeyBytes;
this.$notify( this.activeDid = createPeerDid(publicKeyBytes as Uint8Array);
{ this.credIdHex = cred.credIdHex as string;
group: "modal",
type: "confirm", await accountsDB.open();
title: "No Name", await accountsDB.accounts.add({
text: "You should have a name to attach to this passkey. Would you like to enter your own name first?", dateCreated: new Date().toISOString(),
onNo: async () => { did: this.activeDid,
this.userName = DEFAULT_USERNAME; passkeyCredIdHex: this.credIdHex,
}, publicKeyHex: Buffer.from(publicKeyBytes).toString("hex"),
onYes: async () => { });``
this.$router.push({ name: "new-edit-account" });
},
noText: "try again and use " + DEFAULT_USERNAME,
},
-1,
);
return;
}
const account = await registerAndSavePasskey(
AppString.APP_NAME + " - " + this.userName,
);
this.activeDid = account.did;
this.credIdHex = account.passkeyCredIdHex;
} }
public async createJwtSimplewebauthn() { public async createJwtSimplewebauthn() {
const account: AccountKeyInfo | undefined = await getAccount(
this.activeDid || "",
);
if (!vcLib.isFromPasskey(account)) {
alert(`The DID ${this.activeDid} is not passkey-enabled.`);
return;
}
this.peerSetup = new PeerSetup(); this.peerSetup = new PeerSetup();
this.jwt = await this.peerSetup.createJwtSimplewebauthn( this.jwt = await this.peerSetup.createJwtSimplewebauthn(
this.activeDid as string, this.activeDid as string,
@@ -378,13 +349,6 @@ export default class Help extends Vue {
} }
public async createJwtNavigator() { public async createJwtNavigator() {
const account: AccountKeyInfo | undefined = await getAccount(
this.activeDid || "",
);
if (!vcLib.isFromPasskey(account)) {
alert(`The DID ${this.activeDid} is not passkey-enabled.`);
return;
}
this.peerSetup = new PeerSetup(); this.peerSetup = new PeerSetup();
this.jwt = await this.peerSetup.createJwtNavigator( this.jwt = await this.peerSetup.createJwtNavigator(
this.activeDid as string, this.activeDid as string,
@@ -396,46 +360,44 @@ export default class Help extends Vue {
public async verifyP256() { public async verifyP256() {
const decoded = await verifyJwtP256( const decoded = await verifyJwtP256(
this.credIdHex as string, this.credIdHex as Base64URLString,
this.activeDid as string, this.activeDid as string,
this.peerSetup?.authenticatorData as ArrayBuffer, this.peerSetup.authenticatorData as ArrayBuffer,
this.peerSetup?.challenge as Uint8Array, this.peerSetup.challenge as Uint8Array,
this.peerSetup?.clientDataJsonBase64Url as Base64URLString, this.peerSetup.clientDataJsonBase64Url as Base64URLString,
this.peerSetup?.signature as Base64URLString, this.peerSetup.signature as Base64URLString,
); );
console.log("decoded", decoded); console.log("decoded", decoded);
} }
public async verifySimplewebauthn() { public async verifySimplewebauthn() {
const decoded = await verifyJwtSimplewebauthn( const decoded = await verifyJwtSimplewebauthn(
this.credIdHex as string, this.credIdHex as Base64URLString,
this.activeDid as string, this.activeDid as string,
this.peerSetup?.authenticatorData as ArrayBuffer, this.peerSetup.authenticatorData as ArrayBuffer,
this.peerSetup?.challenge as Uint8Array, this.peerSetup.challenge as Uint8Array,
this.peerSetup?.clientDataJsonBase64Url as Base64URLString, this.peerSetup.clientDataJsonBase64Url as Base64URLString,
this.peerSetup?.signature as Base64URLString, this.peerSetup.signature as Base64URLString,
); );
console.log("decoded", decoded); console.log("decoded", decoded);
} }
public async verifyWebCrypto() { public async verifyWebCrypto() {
const decoded = await verifyJwtWebCrypto( const decoded = await verifyJwtWebCrypto(
this.credIdHex as string, this.credIdHex as Base64URLString,
this.activeDid as string, this.activeDid as string,
this.peerSetup?.authenticatorData as ArrayBuffer, this.peerSetup.authenticatorData as ArrayBuffer,
this.peerSetup?.challenge as Uint8Array, this.peerSetup.challenge as Uint8Array,
this.peerSetup?.clientDataJsonBase64Url as Base64URLString, this.peerSetup.clientDataJsonBase64Url as Base64URLString,
this.peerSetup?.signature as Base64URLString, this.peerSetup.signature as Base64URLString,
); );
console.log("decoded", decoded); console.log("decoded", decoded);
} }
public async verifyMyJwt() { public async verifyMyJwt() {
const did =
"did:peer:0zKMFjvUgYrM1hXwDciYHiA9MxXtJPXnRLJvqoMNAKoDLX9pKMWLb3VDsgua1p2zW1xXRsjZSTNsfvMnNyMS7dB4k7NAhFwL3pXBrBXgyYJ9ri";
const jwt = const jwt =
"eyJ0eXAiOiJKV0FOVCIsImFsZyI6IkVTMjU2In0.eyJBdXRoZW50aWNhdGlvbkRhdGFCNjRVUkwiOiJTWllONVlnT2pHaDBOQmNQWkhaZ1c0X2tycm1paGpMSG1Wenp1b01kbDJNRkFBQUFBQSIsIkNsaWVudERhdGFKU09OQjY0VVJMIjoiZXlKMGVYQmxJam9pZDJWaVlYVjBhRzR1WjJWMElpd2lZMmhoYkd4bGJtZGxJam9pWlhsS01sbDVTVFpsZVVwcVkyMVdhMXBYTlRCaFYwWnpWVE5XYVdGdFZtcGtRMGsyWlhsS1FWa3lPWFZrUjFZMFpFTkpOa2x0YURCa1NFSjZUMms0ZG1NeVRtOWFWekZvVEcwNWVWcDVTWE5KYTBJd1pWaENiRWxxYjJsU01td3lXbFZHYW1SSGJIWmlhVWx6U1cxU2JHTXlUbmxoV0VJd1lWYzVkVWxxYjJsalIydzJaVzFGYVdaWU1ITkpiV3hvWkVOSk5rMVVZM2hQUkZVMFRtcHJOVTFEZDJsaFdFNTZTV3B2YVZwSGJHdFBia0pzV2xoSk5rMUljRXhVVlZweFpHeFdibGRZU2s1TlYyaFpaREJTYW1GV2JFbGhWVVUxVkZob1dXUkZjRkZYUnpWVFZFVndNbU5YT1U1VWEwWk1ZakJTVFZkRWJIZFRNREZZVkVkSmVsWnJVbnBhTTFab1RWaEJlV1ZzWTNobFJtaFRZekp3WVZVeFVrOWpNbG95VkZjMVQyVlZNVlJPTWxKRFRrZHpNMVJyUm05U2JtUk5UVE5DV1ZGdVNrTlhSMlExVjFWdk5XTnRhMmxtVVNJc0ltOXlhV2RwYmlJNkltaDBkSEE2THk5c2IyTmhiR2h2YzNRNk9EQTRNQ0lzSW1OeWIzTnpUM0pwWjJsdUlqcG1ZV3h6WlgwIiwiaWF0IjoxNzE4NTg2OTkyLCJpc3MiOiJkaWQ6cGVlcjowektNRmp2VWdZck0xaFh3RGNpWUhpQTlNeFh0SlBYblJMSnZxb01OQUtvRExYOXBLTVdMYjNWRHNndWExcDJ6VzF4WFJzalpTVE5zZnZNbk55TVM3ZEI0azdOQWhGd0wzcFhCckJYZ3lZSjlyaSJ9.MEUCIQDJyCTbMPIFnuBoW3FYnlgtDEIHZ2OrkCEvqVnHU7kJDQIgVxjBjfW1TwQfcSOYwK8Z7AdCWGJlyxtLEsrnPif7caE"; "eyJ0eXAiOiJKV0FOVCIsImFsZyI6IkVTMjU2In0.eyJBdXRoZW50aWNhdGlvbkRhdGFCNjRVUkwiOiJTWllONVlnT2pHaDBOQmNQWkhaZ1c0X2tycm1paGpMSG1Wenp1b01kbDJNRkFBQUFBQSIsIkNsaWVudERhdGFKU09OQjY0VVJMIjoiZXlKMGVYQmxJam9pZDJWaVlYVjBhRzR1WjJWMElpd2lZMmhoYkd4bGJtZGxJam9pWlhsS01sbDVTVFpsZVVwcVkyMVdhMXBYTlRCaFYwWnpWVE5XYVdGdFZtcGtRMGsyWlhsS1FWa3lPWFZrUjFZMFpFTkpOa2x0YURCa1NFSjZUMms0ZG1NeVRtOWFWekZvVEcwNWVWcDVTWE5KYTBJd1pWaENiRWxxYjJsU01td3lXbFZHYW1SSGJIWmlhVWx6U1cxU2JHTXlUbmxoV0VJd1lWYzVkVWxxYjJsalIydzJaVzFGYVdaWU1ITkpiV3hvWkVOSk5rMVVZM2hQUkZVMFRtcHJOVTFEZDJsaFdFNTZTV3B2YVZwSGJHdFBia0pzV2xoSk5rMUljRXhVVlZweFpHeFdibGRZU2s1TlYyaFpaREJTYW1GV2JFbGhWVVUxVkZob1dXUkZjRkZYUnpWVFZFVndNbU5YT1U1VWEwWk1ZakJTVFZkRWJIZFRNREZZVkVkSmVsWnJVbnBhTTFab1RWaEJlV1ZzWTNobFJtaFRZekp3WVZVeFVrOWpNbG95VkZjMVQyVlZNVlJPTWxKRFRrZHpNMVJyUm05U2JtUk5UVE5DV1ZGdVNrTlhSMlExVjFWdk5XTnRhMmxtVVNJc0ltOXlhV2RwYmlJNkltaDBkSEE2THk5c2IyTmhiR2h2YzNRNk9EQTRNQ0lzSW1OeWIzTnpUM0pwWjJsdUlqcG1ZV3h6WlgwIiwiaWF0IjoxNzE4NTg2OTkyLCJpc3MiOiJkaWQ6cGVlcjowektNRmp2VWdZck0xaFh3RGNpWUhpQTlNeFh0SlBYblJMSnZxb01OQUtvRExYOXBLTVdMYjNWRHNndWExcDJ6VzF4WFJzalpTVE5zZnZNbk55TVM3ZEI0azdOQWhGd0wzcFhCckJYZ3lZSjlyaSJ9.MEUCIQDJyCTbMPIFnuBoW3FYnlgtDEIHZ2OrkCEvqVnHU7kJDQIgVxjBjfW1TwQfcSOYwK8Z7AdCWGJlyxtLEsrnPif7caE";
const pieces = jwt.split("."); const pieces = jwt.split(".");
console.log("pieces", typeof pieces[1], pieces);
const payload = JSON.parse(Buffer.from(pieces[1], "base64").toString()); const payload = JSON.parse(Buffer.from(pieces[1], "base64").toString());
const authData = Buffer.from(payload["AuthenticationDataB64URL"], "base64"); const authData = Buffer.from(payload["AuthenticationDataB64URL"], "base64");
const clientJSON = Buffer.from( const clientJSON = Buffer.from(
@@ -446,8 +408,8 @@ export default class Help extends Vue {
const challenge = clientData.challenge; const challenge = clientData.challenge;
const signatureB64URL = pieces[2]; const signatureB64URL = pieces[2];
const decoded = await verifyJwtWebCrypto( const decoded = await verifyJwtWebCrypto(
this.credIdHex as string, this.credIdHex as Base64URLString,
did, this.activeDid as string,
authData, authData,
challenge, challenge,
payload["ClientDataJSONB64URL"], payload["ClientDataJSONB64URL"],

View File

@@ -16,8 +16,6 @@ export default defineConfig({
srcDir: '.', srcDir: '.',
filename: 'sw_scripts-combined.js', filename: 'sw_scripts-combined.js',
manifest: { manifest: {
// This is used for the app name. It doesn't include a space, because iOS complains if i recall correctly.
// There is a name with spaces in the constants/app.js file for use internally.
name: process.env.TIME_SAFARI_APP_TITLE || require('./package.json').name, name: process.env.TIME_SAFARI_APP_TITLE || require('./package.json').name,
short_name: process.env.TIME_SAFARI_APP_TITLE || require('./package.json').name, short_name: process.env.TIME_SAFARI_APP_TITLE || require('./package.json').name,
// 192x192 and 512x512 are important for Chrome to show that it's installable // 192x192 and 512x512 are important for Chrome to show that it's installable